From 116b70e2a5ffb9d1dd4040938edbac4844dc72d5 Mon Sep 17 00:00:00 2001 From: T Date: Fri, 10 Jul 2026 21:17:04 -0400 Subject: [PATCH 01/89] chore: prepare v3.68.0 release (#875) --- CHANGELOG.md | 19 ++++++++++++++++ README.md | 25 ++++++++------------- locales/ca/README.md | 14 +++++++----- locales/de/README.md | 14 +++++++----- locales/es/README.md | 14 +++++++----- locales/fr/README.md | 14 +++++++----- locales/hi/README.md | 14 +++++++----- locales/id/README.md | 14 +++++++----- locales/it/README.md | 14 +++++++----- locales/ja/README.md | 14 +++++++----- locales/ko/README.md | 14 +++++++----- locales/nl/README.md | 14 +++++++----- locales/pl/README.md | 14 +++++++----- locales/pt-BR/README.md | 14 +++++++----- locales/ru/README.md | 14 +++++++----- locales/tr/README.md | 14 +++++++----- locales/vi/README.md | 14 +++++++----- locales/zh-CN/README.md | 14 +++++++----- locales/zh-TW/README.md | 14 +++++++----- src/core/webview/ClineProvider.ts | 2 +- src/package.json | 2 +- webview-ui/src/i18n/locales/ca/chat.json | 6 ++--- webview-ui/src/i18n/locales/de/chat.json | 6 ++--- webview-ui/src/i18n/locales/en/chat.json | 6 ++--- webview-ui/src/i18n/locales/es/chat.json | 6 ++--- webview-ui/src/i18n/locales/fr/chat.json | 6 ++--- webview-ui/src/i18n/locales/hi/chat.json | 6 ++--- webview-ui/src/i18n/locales/id/chat.json | 6 ++--- webview-ui/src/i18n/locales/it/chat.json | 6 ++--- webview-ui/src/i18n/locales/ja/chat.json | 6 ++--- webview-ui/src/i18n/locales/ko/chat.json | 6 ++--- webview-ui/src/i18n/locales/nl/chat.json | 6 ++--- webview-ui/src/i18n/locales/pl/chat.json | 6 ++--- webview-ui/src/i18n/locales/pt-BR/chat.json | 6 ++--- webview-ui/src/i18n/locales/ru/chat.json | 6 ++--- webview-ui/src/i18n/locales/tr/chat.json | 6 ++--- webview-ui/src/i18n/locales/vi/chat.json | 6 ++--- webview-ui/src/i18n/locales/zh-CN/chat.json | 6 ++--- webview-ui/src/i18n/locales/zh-TW/chat.json | 6 ++--- 39 files changed, 237 insertions(+), 157 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49749bab83..5d0cb7f707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Zoo Code Changelog +## [3.68.0] + +### Minor Changes + +- Add Friendli provider with GLM-5.2 support for another hosted way to use the latest GLM model (#722 by @Lee-Si-Yoon, PR #721 by @Lee-Si-Yoon) +- Add native thinking/reasoning support for Ollama models to preserve reasoning output end-to-end (#831 by @navedmerchant, PR #832 by @navedmerchant) +- Fix(anthropic): honor custom `apiModelId` selections instead of silently defaulting to `claude-sonnet-4-5` (#418 by @tatianadenel-devops, #843 by @grizmin, PR #842 by @grizmin) +- Fix(ollama): correctly handle tool results and prevent premature context condensing (#847 by @navedmerchant, PR #848 by @navedmerchant) +- Improve Anthropic Vertex Claude content block handling for more reliable responses (#788 by @daewoongoh, PR #789 by @daewoongoh) +- Fix(task-lifecycle): preserve the parent-child link when a delegated subtask is interrupted (#560 by @edelauna, PR #787 by @edelauna) +- Refactor: remove the deprecated `openai-error-handler` shim and use the shared `error-handler` directly (#766 by @daewoongoh, PR #767 by @daewoongoh) +- Feat(nightly-publish): publish Open VSX pre-releases and skip nightly publish on release merges (#784 by @edelauna, PR #790 by @edelauna) +- Fix(ci): don't skip fork-PR label reconciliation on scheduled and manual runs (PR #234 by @app/roomote) +- Fix(label-pr-review-state): detect merge conflicts and label PRs with `has-conflicts` (PR #269 by @app/roomote) +- Chore(deps): update the `github/codeql-action` digest to `411c4c9` (PR #803 by @app/renovate) +- Chore(deps): update `@types/react` to `v18.3.31` (PR #805 by @app/renovate) +- Chore(deps): update `axios` to `v1.18.1` (PR #806 by @app/renovate) +- Chore: merge the v3.66.0 release preparation branch into `main` (PR #795 by @navedmerchant) + ## [3.66.0] ### Minor Changes diff --git a/README.md b/README.md index de436e9195..ceffbc6d9a 100644 --- a/README.md +++ b/README.md @@ -53,22 +53,15 @@ You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX) for this exact support, so if you are having problems or if you have question, jump on and ask. -## What's New in v3.66.0 - -- **Claude Sonnet 5 support** — the latest Claude model is now available across Anthropic, Bedrock, and Vertex providers -- **Semble v0.4.1 upgrade** — flattened result parsing and localized status messages -- **Task-lifecycle status transition guard** — a new status transition guard and startup delegation reconciliation prevent invalid task state transitions -- Fix: LiteLLM cache key collision and silent fallback to a non-existent default model -- Fix: reliable auto context condensing for the VS Code Language Model API -- Fix: ThinkingBudget now supports `xhigh` and all extended reasoning effort values -- Fix: round-trip DeepSeek `reasoning_content` in thinking mode to prevent 400 errors -- Fix: base64-encode Gemini `thoughtSignature` bypass token to fix the Vertex AI empty-response loop -- Fix: provider cache reset after settings import -- Fix: atomically serialize `reopenParentFromDelegation` -- Fix: shell default profile name type guard -- Security: dependency-review, invisible-char detection, and least-privilege workflow permissions -- Upgrade `@anthropic-ai/sdk` to 0.104.1 and `@anthropic-ai/vertex-sdk` to 0.17.1 -- Dependency and tooling updates +## What's New in v3.68.0 + +- **Friendli provider with GLM-5.2 support** — use the latest GLM model through Friendli. +- **Native Ollama thinking/reasoning support** — preserve reasoning output end-to-end when you use Ollama models. +- **Anthropic custom `apiModelId` fix** — custom Anthropic model IDs now stay selected instead of silently falling back to `claude-sonnet-4-5`. +- Fix: Ollama provider tool result handling and premature context condensing. +- Fix: preserve the parent-child task link when a delegated subtask is interrupted. +- Improve Anthropic Vertex Claude content block handling for more reliable responses. +- CI, nightly publishing, and dependency/tooling updates.
🌐 Available languages diff --git a/locales/ca/README.md b/locales/ca/README.md index e18ed16083..c58553fb68 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -30,11 +30,15 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o alguna pregunta, entra i pregunta. -## Novetats a la v3.66.0 - -- **Suport per a Claude Sonnet 5** — el model de Claude més recent ja està disponible als proveïdors Anthropic, Bedrock i Vertex -- **Actualització a Semble v0.4.1** — anàlisi de resultats aplanada i missatges d'estat localitzats -- **Guàrdia de transició d'estat del cicle de vida de tasques** — una nova guàrdia de transició d'estat i la reconciliació de delegació a l'inici eviten transicions d'estat de tasques no vàlides +## Novetats a la v3.68.0 + +- **Proveïdor Friendli amb suport per a GLM-5.2** — utilitza el model GLM més recent a través de Friendli. +- **Suport natiu de thinking/reasoning per a Ollama** — conserva la sortida de raonament d'extrem a extrem quan fas servir models d'Ollama. +- **Correcció del `apiModelId` personalitzat d'Anthropic** — els identificadors de model personalitzats d'Anthropic ara es mantenen seleccionats en lloc de tornar en silenci a `claude-sonnet-4-5`. +- Correcció: gestió dels resultats d'eines del proveïdor Ollama i condensació prematura del context. +- Correcció: conserva l'enllaç pare-fill de la tasca quan s'interromp una subtasca delegada. +- Millora la gestió dels blocs de contingut de Claude a Anthropic Vertex per obtenir respostes més fiables. +- Actualitzacions de CI, publicació nightly i dependències/eines. ## Què pot fer Zoo Code per TU? diff --git a/locales/de/README.md b/locales/de/README.md index 9851d9afbe..808b0faf3f 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -30,11 +30,15 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Roo→Zoo-Migrationsleitfaden](https://docs.zoocode.dev/roo-to-zoo-migration). Wir wollen Nutzer beim Umstieg so gut wie möglich unterstützen, und genau dafür sind unser [Reddit](https://www.reddit.com/r/ZooCode) und [Discord](https://discord.gg/VxfP4Vx3gX) da. Wenn du Probleme hast oder Fragen auftauchen, komm vorbei und frag nach. -## Neu in v3.66.0 - -- **Claude Sonnet 5 Unterstützung** — das neueste Claude-Modell ist jetzt bei den Anbietern Anthropic, Bedrock und Vertex verfügbar -- **Semble v0.4.1 Upgrade** — vereinfachte Ergebnisverarbeitung und lokalisierte Statusmeldungen -- **Task-Lifecycle-Zustandsübergangs-Wächter** — ein neuer Zustandsübergangs-Wächter und Startup-Delegationsabgleich verhindern ungültige Task-Zustandsübergänge +## Neu in v3.68.0 + +- **Friendli-Anbieter mit GLM-5.2-Unterstützung** — nutze das neueste GLM-Modell über Friendli. +- **Native Ollama-Unterstützung für thinking/reasoning** — behalte die Reasoning-Ausgabe durchgängig bei, wenn du Ollama-Modelle verwendest. +- **Fix für benutzerdefinierte `apiModelId` bei Anthropic** — benutzerdefinierte Anthropic-Modell-IDs bleiben jetzt ausgewählt, statt stillschweigend auf `claude-sonnet-4-5` zurückzufallen. +- Fix: Behandlung von Tool-Ergebnissen im Ollama-Anbieter und verfrühte Kontextkondensierung. +- Fix: Behalte die Eltern-Kind-Verknüpfung bei, wenn ein delegierter Untertask unterbrochen wird. +- Verbessere die Verarbeitung von Claude-Content-Blöcken in Anthropic Vertex für zuverlässigere Antworten. +- Updates für CI, Nightly-Publishing und Abhängigkeiten/Tooling. ## Was kann Zoo Code für DICH tun? diff --git a/locales/es/README.md b/locales/es/README.md index 0ce1b73ff0..a63742ccaf 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -30,11 +30,15 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guía de migración Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ayudar a los usuarios durante la transición, y para eso tenemos nuestro [Reddit](https://www.reddit.com/r/ZooCode) y [Discord](https://discord.gg/VxfP4Vx3gX). Si tienes problemas o alguna pregunta, entra y pregúntanos. -## Novedades de la v3.66.0 - -- **Soporte para Claude Sonnet 5** — el modelo de Claude más reciente ya está disponible en los proveedores Anthropic, Bedrock y Vertex -- **Actualización a Semble v0.4.1** — análisis de resultados aplanado y mensajes de estado localizados -- **Guardia de transición de estado del ciclo de vida de tareas** — una nueva guardia de transición de estado y la reconciliación de delegación al inicio evitan transiciones de estado de tareas no válidas +## Novedades de la v3.68.0 + +- **Proveedor Friendli con soporte para GLM-5.2** — usa el modelo GLM más reciente a través de Friendli. +- **Compatibilidad nativa de thinking/reasoning para Ollama** — conserva la salida de razonamiento de extremo a extremo cuando uses modelos de Ollama. +- **Corrección de `apiModelId` personalizado de Anthropic** — los IDs de modelo personalizados de Anthropic ahora permanecen seleccionados en lugar de volver silenciosamente a `claude-sonnet-4-5`. +- Corrección: manejo de resultados de herramientas del proveedor Ollama y condensación prematura del contexto. +- Corrección: conserva el vínculo padre-hijo de la tarea cuando se interrumpe una subtarea delegada. +- Mejora el manejo de bloques de contenido de Claude en Anthropic Vertex para obtener respuestas más fiables. +- Actualizaciones de CI, publicación nightly y dependencias/herramientas. ## ¿Qué puede hacer Zoo Code por TI? diff --git a/locales/fr/README.md b/locales/fr/README.md index 4b86362eed..a164e50050 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -30,11 +30,15 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [guide de migration Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). On veut aider au maximum pendant la transition, et notre [Reddit](https://www.reddit.com/r/ZooCode) et notre [Discord](https://discord.gg/VxfP4Vx3gX) sont là pour ça. Si tu rencontres un problème ou si tu as une question, viens demander. -## Nouveautés de la v3.66.0 - -- **Prise en charge de Claude Sonnet 5** — le dernier modèle de Claude est désormais disponible auprès des fournisseurs Anthropic, Bedrock et Vertex -- **Mise à niveau vers Semble v0.4.1** — analyse aplatie des résultats et messages d'état localisés -- **Garde de transition d'état du cycle de vie des tâches** — une nouvelle garde de transition d'état et la réconciliation de délégation au démarrage empêchent les transitions d'état de tâche invalides +## Nouveautés de la v3.68.0 + +- **Provider Friendli avec prise en charge de GLM-5.2** — utilise le dernier modèle GLM via Friendli. +- **Prise en charge native du thinking/reasoning pour Ollama** — préserve la sortie de raisonnement de bout en bout quand tu utilises des modèles Ollama. +- **Correction du `apiModelId` personnalisé d'Anthropic** — les identifiants de modèle Anthropic personnalisés restent désormais sélectionnés au lieu de revenir silencieusement à `claude-sonnet-4-5`. +- Correctif : gestion des résultats d'outils du fournisseur Ollama et condensation prématurée du contexte. +- Correctif : préserve le lien parent-enfant de la tâche lorsqu'une sous-tâche déléguée est interrompue. +- Améliore la gestion des blocs de contenu Claude dans Anthropic Vertex pour des réponses plus fiables. +- Mises à jour de la CI, de la publication nightly et des dépendances/outils. ## Que peut faire Zoo Code pour VOUS ? diff --git a/locales/hi/README.md b/locales/hi/README.md index 3c11c7dd9e..a9ccd13cc3 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -30,11 +30,15 @@ Roo Code से Zoo Code में आने के लिए एक quick guide तुम्हें [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration) में मिल जाएगी। We plan to help users as much as possible during the transition, और उसी support के लिए हमारा [Reddit](https://www.reddit.com/r/ZooCode) और [Discord](https://discord.gg/VxfP4Vx3gX) है। अगर तुम्हें कोई problem हो या कोई question हो, आकर पूछो। -## v3.66.0 में नया क्या है - -- **Claude Sonnet 5 समर्थन** — नवीनतम Claude मॉडल अब Anthropic, Bedrock और Vertex प्रदाताओं पर उपलब्ध है -- **Semble v0.4.1 अपग्रेड** — चपटा परिणाम पार्सिंग और स्थानीयकृत स्थिति संदेश -- **कार्य-जीवनचक्र स्थिति संक्रमण गार्ड** — एक नया स्थिति संक्रमण गार्ड और स्टार्टअप प्रतिनिधिमंडल मेल-मिलाप अमान्य कार्य स्थिति संक्रमण को रोकता है +## v3.68.0 में नया क्या है + +- **GLM-5.2 समर्थन के साथ Friendli प्रदाता** — Friendli के जरिए नवीनतम GLM मॉडल का उपयोग करें। +- **Ollama के लिए नेटिव thinking/reasoning समर्थन** — Ollama मॉडल इस्तेमाल करते समय reasoning आउटपुट को शुरू से अंत तक सुरक्षित रखें। +- **Anthropic कस्टम `apiModelId` फिक्स** — कस्टम Anthropic मॉडल ID अब चुपचाप `claude-sonnet-4-5` पर वापस जाने के बजाय चुनी हुई रहती हैं। +- फिक्स: Ollama प्रदाता में tool result हैंडलिंग और समय से पहले context condensing। +- फिक्स: delegated subtask रुकने पर parent-child task link को बनाए रखें। +- अधिक विश्वसनीय responses के लिए Anthropic Vertex Claude content block handling में सुधार। +- CI, nightly publishing, और dependency/tooling अपडेट्स। ## Zoo Code आपके लिए क्या कर सकता है? diff --git a/locales/id/README.md b/locales/id/README.md index 2d0a423bf4..0e2d652969 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -30,11 +30,15 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di [panduan migrasi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Kami ingin membantu pengguna semaksimal mungkin selama masa transisi, dan itulah gunanya [Reddit](https://www.reddit.com/r/ZooCode) dan [Discord](https://discord.gg/VxfP4Vx3gX) kami. Kalau kamu mengalami masalah atau punya pertanyaan, langsung mampir dan tanya. -## Yang Baru di v3.66.0 - -- **Dukungan Claude Sonnet 5** — model Claude terbaru kini tersedia di penyedia Anthropic, Bedrock, dan Vertex -- **Peningkatan ke Semble v0.4.1** — penguraian hasil yang diratakan dan pesan status terlokalisasi -- **Penjaga transisi status siklus hidup tugas** — penjaga transisi status baru dan rekonsiliasi delegasi saat startup mencegah transisi status tugas yang tidak valid +## Yang Baru di v3.68.0 + +- **Provider Friendli dengan dukungan GLM-5.2** — gunakan model GLM terbaru lewat Friendli. +- **Dukungan thinking/reasoning native untuk Ollama** — pertahankan keluaran penalaran dari awal hingga akhir saat kamu memakai model Ollama. +- **Perbaikan `apiModelId` kustom Anthropic** — ID model Anthropic kustom kini tetap terpilih alih-alih diam-diam kembali ke `claude-sonnet-4-5`. +- Perbaikan: penanganan hasil tool penyedia Ollama dan pemadatan konteks yang terlalu dini. +- Perbaikan: pertahankan tautan induk-anak tugas saat subtugas yang didelegasikan terputus. +- Peningkatan penanganan blok konten Claude Anthropic Vertex untuk respons yang lebih andal. +- Pembaruan CI, publikasi nightly, dan dependensi/tooling. ## Apa yang Bisa Zoo Code Lakukan Untuk ANDA? diff --git a/locales/it/README.md b/locales/it/README.md index 543dc0b5d9..6f47b7d0d6 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -30,11 +30,15 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida alla migrazione Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Vogliamo aiutare gli utenti il più possibile durante la transizione, e per questo abbiamo il nostro [Reddit](https://www.reddit.com/r/ZooCode) e il nostro [Discord](https://discord.gg/VxfP4Vx3gX). Se hai problemi o domande, passa pure e chiedi. -## Novità in v3.66.0 - -- **Supporto per Claude Sonnet 5** — l'ultimo modello di Claude è ora disponibile sui provider Anthropic, Bedrock e Vertex -- **Aggiornamento a Semble v0.4.1** — analisi dei risultati appiattita e messaggi di stato localizzati -- **Guardia di transizione di stato del ciclo di vita delle attività** — una nuova guardia di transizione di stato e la riconciliazione della delega all'avvio prevengono transizioni di stato delle attività non valide +## Novità in v3.68.0 + +- **Provider Friendli con supporto GLM-5.2** — usa il modello GLM più recente tramite Friendli. +- **Supporto nativo thinking/reasoning per Ollama** — conserva l'output di ragionamento end-to-end quando usi i modelli Ollama. +- **Correzione del `apiModelId` personalizzato di Anthropic** — gli ID modello Anthropic personalizzati ora restano selezionati invece di tornare silenziosamente a `claude-sonnet-4-5`. +- Correzione: gestione dei risultati degli strumenti del provider Ollama e condensazione prematura del contesto. +- Correzione: mantieni il collegamento padre-figlio dell'attività quando una sottoattività delegata viene interrotta. +- Migliora la gestione dei blocchi di contenuto Claude in Anthropic Vertex per risposte più affidabili. +- Aggiornamenti a CI, pubblicazione nightly e dipendenze/tooling. ## Cosa può fare Zoo Code per TE? diff --git a/locales/ja/README.md b/locales/ja/README.md index 7b5781c30a..0dfe4ecbd5 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -30,11 +30,15 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo→Zoo 移行ガイド](https://docs.zoocode.dev/roo-to-zoo-migration) で確認できます。移行中のユーザーをできるだけ支援したいと考えていて、そのために [Reddit](https://www.reddit.com/r/ZooCode) と [Discord](https://discord.gg/VxfP4Vx3gX) を用意しています。困ったことや質問があれば、気軽に参加して聞いてください。 -## v3.66.0 の新機能 - -- **Claude Sonnet 5 サポート** — 最新の Claude モデルが Anthropic、Bedrock、Vertex の各プロバイダーで利用可能になりました -- **Semble v0.4.1 へのアップグレード** — 平坦な結果解析とローカライズされたステータスメッセージ -- **タスクライフサイクル状態遷移ガード** — 新しい状態遷移ガードと起動時の委任調整により、無効なタスク状態遷移を防ぎます +## v3.68.0 の新機能 + +- **GLM-5.2 をサポートした Friendli プロバイダー** — Friendli 経由で最新の GLM モデルを使えます。 +- **Ollama のネイティブ thinking/reasoning サポート** — Ollama モデル使用時に reasoning 出力をエンドツーエンドで保持します。 +- **Anthropic のカスタム `apiModelId` 修正** — カスタム Anthropic モデル ID が `claude-sonnet-4-5` に黙ってフォールバックせず、そのまま選択された状態を維持します。 +- 修正: Ollama プロバイダーのツール結果処理と早すぎるコンテキスト圧縮。 +- 修正: 委譲されたサブタスクが中断されたときに親子タスクのリンクを維持。 +- Anthropic Vertex での Claude コンテンツブロック処理を改善し、応答の信頼性を向上。 +- CI、nightly 公開、依存関係/ツール更新。 ## Zoo Codeがあなたのためにできること diff --git a/locales/ko/README.md b/locales/ko/README.md index 77fe7377ca..33b19fcfc8 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -30,11 +30,15 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그레이션 가이드](https://docs.zoocode.dev/roo-to-zoo-migration)에서 확인할 수 있어. 전환하는 동안 사용자들을 최대한 돕고 싶고, 바로 그 지원을 위해 [Reddit](https://www.reddit.com/r/ZooCode)와 [Discord](https://discord.gg/VxfP4Vx3gX)를 운영하고 있어. 문제가 있거나 궁금한 점이 있으면 들어와서 편하게 물어봐. -## v3.66.0의 새로운 기능 - -- **Claude Sonnet 5 지원** — 최신 Claude 모델이 이제 Anthropic, Bedrock, Vertex 프로바이더에서 사용 가능합니다 -- **Semble v0.4.1 업그레이드** — 평면화된 결과 파싱과 현지화된 상태 메시지 -- **태스크 수명주기 상태 전환 가드** — 새로운 상태 전환 가드와 시작 시 위임 조정으로 잘못된 태스크 상태 전환을 방지합니다 +## v3.68.0의 새로운 기능 + +- **GLM-5.2를 지원하는 Friendli 프로바이더** — Friendli를 통해 최신 GLM 모델을 사용할 수 있습니다. +- **Ollama용 네이티브 thinking/reasoning 지원** — Ollama 모델을 사용할 때 reasoning 출력을 처음부터 끝까지 유지합니다. +- **Anthropic 사용자 지정 `apiModelId` 수정** — 사용자 지정 Anthropic 모델 ID가 이제 조용히 `claude-sonnet-4-5`로 되돌아가지 않고 선택된 상태로 유지됩니다. +- 수정: Ollama 프로바이더의 도구 결과 처리 및 너무 이른 컨텍스트 압축. +- 수정: 위임된 하위 작업이 중단될 때 부모-자식 작업 연결을 유지합니다. +- Anthropic Vertex Claude 콘텐츠 블록 처리를 개선해 더 안정적인 응답을 제공합니다. +- CI, nightly 배포, 의존성/툴링 업데이트. ## Zoo Code가 당신을 위해 무엇을 할 수 있을까요? diff --git a/locales/nl/README.md b/locales/nl/README.md index b428143c57..de7cdd352b 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -30,11 +30,15 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de [Roo→Zoo-migratiegids](https://docs.zoocode.dev/roo-to-zoo-migration). We willen gebruikers zo goed mogelijk helpen tijdens de overgang, en precies daarvoor zijn onze [Reddit](https://www.reddit.com/r/ZooCode) en [Discord](https://discord.gg/VxfP4Vx3gX) er. Als je ergens tegenaan loopt of vragen hebt, kom langs en vraag het. -## Nieuw in v3.66.0 - -- **Claude Sonnet 5 ondersteuning** — het nieuwste Claude-model is nu beschikbaar bij de Anthropic, Bedrock en Vertex providers -- **Semble v0.4.1 upgrade** — afgevlakte resultaatverwerking en gelokaliseerde statusberichten -- **Taak-levenscyclus statusovergangsbewaking** — een nieuwe statusovergangsbewaking en opstart-delegatieverzoening voorkomen ongeldige taakstatusovergangen +## Nieuw in v3.68.0 + +- **Friendli-provider met GLM-5.2-ondersteuning** — gebruik het nieuwste GLM-model via Friendli. +- **Native thinking/reasoning-ondersteuning voor Ollama** — behoud reasoning-uitvoer van begin tot eind wanneer je Ollama-modellen gebruikt. +- **Fix voor aangepaste Anthropic-`apiModelId`** — aangepaste Anthropic-model-ID's blijven nu geselecteerd in plaats van stilletjes terug te vallen op `claude-sonnet-4-5`. +- Fix: afhandeling van toolresultaten in de Ollama-provider en voortijdige contextcompressie. +- Fix: behoud de ouder-kind-koppeling van taken wanneer een gedelegeerde subtaak wordt onderbroken. +- Verbeterde verwerking van Claude-contentblokken in Anthropic Vertex voor betrouwbaardere antwoorden. +- Updates voor CI, nightly-publicatie en dependencies/tooling. ## Wat kan Zoo Code voor JOU doen? diff --git a/locales/pl/README.md b/locales/pl/README.md index a69fa23ff3..deba8b5bca 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -30,11 +30,15 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku migracji Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chcemy jak najlepiej pomagać użytkownikom w czasie przejścia i właśnie do tego służą nasze [Reddit](https://www.reddit.com/r/ZooCode) oraz [Discord](https://discord.gg/VxfP4Vx3gX). Jeśli masz problem albo pytanie, wpadaj i pytaj. -## Nowości w v3.66.0 - -- **Obsługa Claude Sonnet 5** — najnowszy model Claude jest teraz dostępny u dostawców Anthropic, Bedrock i Vertex -- **Aktualizacja do Semble v0.4.1** — spłaszczony parsing wyników i zlokalizowane komunikaty stanu -- **Strażnik przejścia stanu cyklu życia zadań** — nowy strażnik przejścia stanu i uzgadnianie delegacji przy uruchomieniu zapobiegają nieprawidłowym przejściom stanu zadań +## Nowości w v3.68.0 + +- **Provider Friendli z obsługą GLM-5.2** — używaj najnowszego modelu GLM przez Friendli. +- **Natywna obsługa thinking/reasoning dla Ollamy** — zachowuj wynik rozumowania od początku do końca podczas korzystania z modeli Ollama. +- **Poprawka niestandardowego `apiModelId` Anthropic** — niestandardowe identyfikatory modeli Anthropic pozostają teraz wybrane zamiast po cichu wracać do `claude-sonnet-4-5`. +- Poprawka: obsługa wyników narzędzi w providerze Ollama i przedwczesne kondensowanie kontekstu. +- Poprawka: zachowuj powiązanie zadania nadrzędnego i podrzędnego, gdy delegowane podzadanie zostanie przerwane. +- Ulepszona obsługa bloków treści Claude w Anthropic Vertex dla bardziej niezawodnych odpowiedzi. +- Aktualizacje CI, publikacji nightly oraz zależności/narzędzi. ## Co Zoo Code może zrobić dla CIEBIE? diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index d39d4429cc..5f6c10e81e 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -30,11 +30,15 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia de migração Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ajudar os usuários durante essa transição da melhor forma possível, e é exatamente para isso que temos nosso [Reddit](https://www.reddit.com/r/ZooCode) e nosso [Discord](https://discord.gg/VxfP4Vx3gX). Se você tiver algum problema ou dúvida, apareça por lá e pergunte. -## Novidades na v3.66.0 - -- **Suporte ao Claude Sonnet 5** — o modelo mais recente do Claude agora está disponível nos provedores Anthropic, Bedrock e Vertex -- **Atualização para Semble v0.4.1** — análise de resultados achatada e mensagens de status localizadas -- **Guarda de transição de estado do ciclo de vida das tarefas** — uma nova guarda de transição de estado e a reconciliação de delegação na inicialização evitam transições de estado de tarefa inválidas +## Novidades na v3.68.0 + +- **Provedor Friendli com suporte a GLM-5.2** — use o modelo GLM mais recente pelo Friendli. +- **Suporte nativo de thinking/reasoning para Ollama** — preserve a saída de raciocínio de ponta a ponta ao usar modelos do Ollama. +- **Correção de `apiModelId` personalizado da Anthropic** — os IDs de modelo personalizados da Anthropic agora permanecem selecionados em vez de voltar silenciosamente para `claude-sonnet-4-5`. +- Correção: tratamento de resultados de ferramentas do provedor Ollama e condensação prematura de contexto. +- Correção: preserve o vínculo pai-filho da tarefa quando uma subtarefa delegada é interrompida. +- Melhora o tratamento de blocos de conteúdo Claude no Anthropic Vertex para respostas mais confiáveis. +- Atualizações de CI, publicação nightly e dependências/tooling. ## O que o Zoo Code pode fazer por VOCÊ? diff --git a/locales/ru/README.md b/locales/ru/README.md index f298d42e29..850b25b3e0 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -30,11 +30,15 @@ Короткое руководство по переходу с Roo Code на Zoo Code можно найти в [гайде по миграции Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Мы хотим как можно лучше помочь пользователям во время перехода, и именно для этого у нас есть [Reddit](https://www.reddit.com/r/ZooCode) и [Discord](https://discord.gg/VxfP4Vx3gX). Если у тебя возникнут проблемы или вопросы, заходи и спрашивай. -## Что нового в v3.66.0 - -- **Поддержка Claude Sonnet 5** — новейшая модель Claude теперь доступна у провайдеров Anthropic, Bedrock и Vertex -- **Обновление до Semble v0.4.1** — плоский разбор результатов и локализованные сообщения о статусе -- **Страж перехода состояния жизненного цикла задач** — новый страж перехода состояния и согласование делегирования при запуске предотвращают недопустимые переходы состояния задач +## Что нового в v3.68.0 + +- **Провайдер Friendli с поддержкой GLM-5.2** — используй новейшую модель GLM через Friendli. +- **Нативная поддержка thinking/reasoning для Ollama** — сохраняй вывод рассуждений от начала до конца при использовании моделей Ollama. +- **Исправление пользовательского `apiModelId` Anthropic** — пользовательские идентификаторы моделей Anthropic теперь остаются выбранными вместо тихого возврата к `claude-sonnet-4-5`. +- Исправление: обработка результатов инструментов провайдера Ollama и преждевременное сжатие контекста. +- Исправление: сохраняй связь родительской и дочерней задачи, когда делегированная подзадача прерывается. +- Улучшена обработка блоков контента Claude в Anthropic Vertex для более надёжных ответов. +- Обновления CI, nightly-публикации и зависимостей/инструментов. ## Что Zoo Code может сделать для ВАС? diff --git a/locales/tr/README.md b/locales/tr/README.md index b9bb894607..d95e0e0c5d 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -30,11 +30,15 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da sorunun olursa gel ve sor. -## v3.66.0'daki Yenilikler - -- **Claude Sonnet 5 desteği** — en yeni Claude modeli artık Anthropic, Bedrock ve Vertex sağlayıcılarında kullanılabilir -- **Semble v0.4.1 yükseltmesi** — düzleştirilmiş sonuç ayrıştırma ve yerelleştirilmiş durum mesajları -- **Görev yaşam döngüsü durum geçiş koruması** — yeni bir durum geçiş koruması ve başlangıç yetkilendirme uzlaştırması geçersiz görev durum geçişlerini önler +## v3.68.0'daki Yenilikler + +- **GLM-5.2 destekli Friendli sağlayıcısı** — en yeni GLM modelini Friendli üzerinden kullan. +- **Ollama için yerel thinking/reasoning desteği** — Ollama modellerini kullanırken reasoning çıktısını uçtan uca koru. +- **Anthropic özel `apiModelId` düzeltmesi** — özel Anthropic model kimlikleri artık sessizce `claude-sonnet-4-5` modeline geri dönmek yerine seçili kalıyor. +- Düzeltme: Ollama sağlayıcısında araç sonucu işleme ve erken bağlam yoğunlaştırma. +- Düzeltme: yetkilendirilen bir alt görev kesintiye uğradığında görev üst-alt bağlantısını koru. +- Daha güvenilir yanıtlar için Anthropic Vertex Claude içerik bloğu işleme iyileştirildi. +- CI, nightly yayınlama ve bağımlılık/araç güncellemeleri. ## Zoo Code SİZİN İçin Ne Yapabilir? diff --git a/locales/vi/README.md b/locales/vi/README.md index 0c1228b3f4..27217bc3bc 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -30,11 +30,15 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo Code trong [hướng dẫn chuyển đổi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chúng tôi muốn hỗ trợ người dùng nhiều nhất có thể trong quá trình chuyển đổi, và đó chính là lý do chúng tôi có [Reddit](https://www.reddit.com/r/ZooCode) và [Discord](https://discord.gg/VxfP4Vx3gX). Nếu bạn gặp vấn đề hoặc có câu hỏi, cứ vào hỏi nhé. -## Điểm mới trong v3.66.0 - -- **Hỗ trợ Claude Sonnet 5** — mô hình Claude mới nhất hiện có sẵn trên các nhà cung cấp Anthropic, Bedrock và Vertex -- **Nâng cấp lên Semble v0.4.1** — phân tích kết quả làm phẳng và thông điệp trạng thái đã địa phương hóa -- **Bảo vệ chuyển đổi trạng thái vòng đời tác vụ** — một bảo vệ chuyển đổi trạng thái mới và đối chiếu ủy quyền khi khởi động ngăn chặn các chuyển đổi trạng thái tác vụ không hợp lệ +## Điểm mới trong v3.68.0 + +- **Nhà cung cấp Friendli với hỗ trợ GLM-5.2** — dùng mẫu GLM mới nhất qua Friendli. +- **Hỗ trợ thinking/reasoning gốc cho Ollama** — giữ đầu ra suy luận xuyên suốt khi bạn dùng các mẫu Ollama. +- **Sửa lỗi `apiModelId` tùy chỉnh của Anthropic** — ID mẫu Anthropic tùy chỉnh giờ sẽ tiếp tục được chọn thay vì âm thầm rơi về `claude-sonnet-4-5`. +- Sửa lỗi: xử lý kết quả công cụ của nhà cung cấp Ollama và cô đọng ngữ cảnh quá sớm. +- Sửa lỗi: giữ liên kết cha-con của tác vụ khi một tác vụ con được ủy quyền bị gián đoạn. +- Cải thiện xử lý khối nội dung Claude trên Anthropic Vertex để có phản hồi đáng tin cậy hơn. +- Cập nhật CI, phát hành nightly và phụ thuộc/công cụ. ## Zoo Code có thể làm gì cho BẠN? diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index cb73dd2406..7b6b88a5de 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -30,11 +30,15 @@ 你可以在 [Roo→Zoo 迁移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到从 Roo Code 迁移到 Zoo Code 的快速说明。我们希望在大家迁移过程中尽可能提供帮助,这也是我们设立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社区的原因。如果你遇到问题或有任何疑问,欢迎加入后直接提问。 -## v3.66.0 新增内容 - -- **Claude Sonnet 5 支持** — 最新 Claude 模型现已在 Anthropic、Bedrock 和 Vertex 提供商上可用 -- **Semble v0.4.1 升级** — 扁平化的结果解析和本地化的状态消息 -- **任务生命周期状态转换守卫** — 新的状态转换守卫和启动时的委派对账可防止无效的任务状态转换 +## v3.68.0 新增内容 + +- **支持 GLM-5.2 的 Friendli 提供商** — 通过 Friendli 使用最新的 GLM 模型。 +- **Ollama 原生 thinking/reasoning 支持** — 使用 Ollama 模型时可端到端保留 reasoning 输出。 +- **Anthropic 自定义 `apiModelId` 修复** — 自定义 Anthropic 模型 ID 现在会保持选中,不再静默回退到 `claude-sonnet-4-5`。 +- 修复:Ollama 提供商的工具结果处理和过早的上下文压缩。 +- 修复:委派子任务被中断时保留父子任务链接。 +- 改进 Anthropic Vertex 中 Claude 内容块的处理,以获得更可靠的响应。 +- CI、nightly 发布以及依赖/工具更新。 ## Zoo Code 能为您做什么? diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 6bbaa3ff8a..988c42cbbe 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -30,11 +30,15 @@ 你可以在 [Roo→Zoo 遷移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到從 Roo Code 遷移到 Zoo Code 的快速說明。我們希望在大家轉移過程中盡可能提供協助,這也是我們設立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社群的原因。如果你遇到問題或有任何疑問,歡迎加入後直接提問。 -## v3.66.0 新功能 - -- **Claude Sonnet 5 支援** — 最新 Claude 模型現已於 Anthropic、Bedrock 和 Vertex 供應商可用 -- **Semble v0.4.1 升級** — 扁平化的結果解析和本地化的狀態訊息 -- **任務生命週期狀態轉換守衛** — 新的狀態轉換守衛和啟動時的委派對帳可防止無效的任務狀態轉換 +## v3.68.0 新功能 + +- **支援 GLM-5.2 的 Friendli 供應商** — 透過 Friendli 使用最新的 GLM 模型。 +- **Ollama 原生 thinking/reasoning 支援** — 使用 Ollama 模型時可端對端保留 reasoning 輸出。 +- **Anthropic 自訂 `apiModelId` 修正** — 自訂 Anthropic 模型 ID 現在會保持選取,不再靜默回退到 `claude-sonnet-4-5`。 +- 修正:Ollama 供應商的工具結果處理與過早的內容壓縮。 +- 修正:委派子任務被中斷時保留父子任務連結。 +- 改進 Anthropic Vertex 中 Claude 內容區塊的處理,以獲得更可靠的回應。 +- CI、nightly 發布以及相依套件/工具更新。 ## Zoo Code 能為您做什麼? diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 36f64ffd93..6e5545a1f2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -213,7 +213,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jul-2026-v3.66.0-claude-sonnet-5-semble-task-lifecycle" // v3.66.0 Claude Sonnet 5 support, Semble v0.4.1 upgrade, task-lifecycle status transition guard + public readonly latestAnnouncementId = "jul-2026-v3.68.0-friendli-ollama-anthropic-apimodelid" // v3.68.0 Friendli GLM-5.2 support, native Ollama thinking/reasoning, Anthropic custom apiModelId fix public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager diff --git a/src/package.json b/src/package.json index 76d577e584..181f30122e 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "ZooCodeOrganization", - "version": "3.66.0", + "version": "3.68.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index c4dbfb560c..50cf831543 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -341,9 +341,9 @@ "support": "Dona suport a Zoo Code fent-nos una estrella a GitHub.", "release": { "heading": "Novetats:", - "highlight1": "Suport per a Claude Sonnet 5 — el model de Claude més recent ja està disponible als proveïdors Anthropic, Bedrock i Vertex.", - "highlight2": "Actualització a Semble v0.4.1 — anàlisi de resultats aplanada i missatges d'estat localitzats.", - "highlight3": "Guàrdia de transició d'estat del cicle de vida de tasques — una nova guàrdia de transició d'estat i la reconciliació de delegació a l'inici eviten transicions d'estat de tasques no vàlides." + "highlight1": "Proveïdor Friendli amb suport per a GLM-5.2 — utilitza el model GLM més recent a través de Friendli.", + "highlight2": "Suport natiu de thinking/reasoning per a Ollama — conserva la sortida de raonament d'extrem a extrem quan fas servir models d'Ollama.", + "highlight3": "Correcció del `apiModelId` personalitzat d'Anthropic — els identificadors de model personalitzats d'Anthropic ara es mantenen seleccionats en lloc de tornar en silenci a `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Novetats al núvol:", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 794485f044..be45daca7a 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -341,9 +341,9 @@ "support": "Bitte unterstütze Zoo Code, indem du uns auf GitHub einen Stern gibst.", "release": { "heading": "Was ist neu:", - "highlight1": "Claude Sonnet 5 Unterstützung — das neueste Claude-Modell ist jetzt bei den Anbietern Anthropic, Bedrock und Vertex verfügbar.", - "highlight2": "Semble v0.4.1 Upgrade — vereinfachte Ergebnisverarbeitung und lokalisierte Statusmeldungen.", - "highlight3": "Task-Lifecycle-Zustandsübergangs-Wächter — ein neuer Zustandsübergangs-Wächter und Startup-Delegationsabgleich verhindern ungültige Task-Zustandsübergänge." + "highlight1": "Friendli-Anbieter mit GLM-5.2-Unterstützung — nutze das neueste GLM-Modell über Friendli.", + "highlight2": "Native Ollama-Unterstützung für thinking/reasoning — behalte die Reasoning-Ausgabe durchgängig bei, wenn du Ollama-Modelle verwendest.", + "highlight3": "Fix für benutzerdefinierte `apiModelId` bei Anthropic — benutzerdefinierte Anthropic-Modell-IDs bleiben jetzt ausgewählt, statt stillschweigend auf `claude-sonnet-4-5` zurückzufallen." }, "cloudAgents": { "heading": "Neu in der Cloud:", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index c6886dbf56..f217fbf0b8 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -360,9 +360,9 @@ "support": "Please support Zoo Code by starring us on GitHub.", "release": { "heading": "What's New:", - "highlight1": "Claude Sonnet 5 support — the latest Claude model is now available across Anthropic, Bedrock, and Vertex providers.", - "highlight2": "Semble v0.4.1 upgrade — flattened result parsing and localized status messages.", - "highlight3": "Task-lifecycle status transition guard — a new status transition guard and startup delegation reconciliation prevent invalid task state transitions." + "highlight1": "Friendli provider with GLM-5.2 support — use the latest GLM model through Friendli.", + "highlight2": "Native Ollama thinking/reasoning support — preserve reasoning output end-to-end when you use Ollama models.", + "highlight3": "Anthropic custom `apiModelId` fix — custom Anthropic model IDs now stay selected instead of silently falling back to `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "New in the Cloud:", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 176e1f74f0..2e09e6cc78 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -341,9 +341,9 @@ "support": "Apoya a Zoo Code dándonos una estrella en GitHub.", "release": { "heading": "Novedades:", - "highlight1": "Soporte para Claude Sonnet 5 — el modelo de Claude más reciente ya está disponible en los proveedores Anthropic, Bedrock y Vertex.", - "highlight2": "Actualización a Semble v0.4.1 — análisis de resultados aplanado y mensajes de estado localizados.", - "highlight3": "Guardia de transición de estado del ciclo de vida de tareas — una nueva guardia de transición de estado y la reconciliación de delegación al inicio evitan transiciones de estado de tareas no válidas." + "highlight1": "Proveedor Friendli con soporte para GLM-5.2 — usa el modelo GLM más reciente a través de Friendli.", + "highlight2": "Compatibilidad nativa de thinking/reasoning para Ollama — conserva la salida de razonamiento de extremo a extremo cuando uses modelos de Ollama.", + "highlight3": "Corrección de `apiModelId` personalizado de Anthropic — los IDs de modelo personalizados de Anthropic ahora permanecen seleccionados en lugar de volver silenciosamente a `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Novedades en la Nube:", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 5b20fd0630..363977ddce 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -341,9 +341,9 @@ "support": "Soutiens Zoo Code en nous donnant une étoile sur GitHub.", "release": { "heading": "Nouveautés :", - "highlight1": "Prise en charge de Claude Sonnet 5 — le dernier modèle de Claude est désormais disponible auprès des fournisseurs Anthropic, Bedrock et Vertex.", - "highlight2": "Mise à niveau vers Semble v0.4.1 — analyse aplatie des résultats et messages d'état localisés.", - "highlight3": "Garde de transition d'état du cycle de vie des tâches — une nouvelle garde de transition d'état et la réconciliation de délégation au démarrage empêchent les transitions d'état de tâche invalides." + "highlight1": "Provider Friendli avec prise en charge de GLM-5.2 — utilise le dernier modèle GLM via Friendli.", + "highlight2": "Prise en charge native du thinking/reasoning pour Ollama — préserve la sortie de raisonnement de bout en bout quand tu utilises des modèles Ollama.", + "highlight3": "Correction du `apiModelId` personnalisé d'Anthropic — les identifiants de modèle Anthropic personnalisés restent désormais sélectionnés au lieu de revenir silencieusement à `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Nouveautés dans le Cloud :", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 3eec53911f..39f74b6309 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -341,9 +341,9 @@ "support": "कृपया GitHub पर हमें स्टार देकर Zoo Code का समर्थन करें।", "release": { "heading": "नया क्या है:", - "highlight1": "Claude Sonnet 5 समर्थन — नवीनतम Claude मॉडल अब Anthropic, Bedrock और Vertex प्रदाताओं पर उपलब्ध है।", - "highlight2": "Semble v0.4.1 अपग्रेड — चपटा परिणाम पार्सिंग और स्थानीयकृत स्थिति संदेश।", - "highlight3": "कार्य-जीवनचक्र स्थिति संक्रमण गार्ड — एक नया स्थिति संक्रमण गार्ड और स्टार्टअप प्रतिनिधिमंडल मेल-मिलाप अमान्य कार्य स्थिति संक्रमण को रोकता है।" + "highlight1": "GLM-5.2 समर्थन के साथ Friendli प्रदाता — Friendli के जरिए नवीनतम GLM मॉडल का उपयोग करें।", + "highlight2": "Ollama के लिए नेटिव thinking/reasoning समर्थन — Ollama मॉडल इस्तेमाल करते समय reasoning आउटपुट को शुरू से अंत तक सुरक्षित रखें।", + "highlight3": "Anthropic कस्टम `apiModelId` फिक्स — कस्टम Anthropic मॉडल ID अब चुपचाप `claude-sonnet-4-5` पर वापस जाने के बजाय चुनी हुई रहती हैं।" }, "cloudAgents": { "heading": "क्लाउड में नया:", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 3068e99ad9..6eb35738f3 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -370,9 +370,9 @@ "support": "Dukung Zoo Code dengan memberi kami bintang di GitHub.", "release": { "heading": "Yang Baru:", - "highlight1": "Dukungan Claude Sonnet 5 — model Claude terbaru kini tersedia di penyedia Anthropic, Bedrock, dan Vertex.", - "highlight2": "Peningkatan ke Semble v0.4.1 — penguraian hasil yang diratakan dan pesan status terlokalisasi.", - "highlight3": "Penjaga transisi status siklus hidup tugas — penjaga transisi status baru dan rekonsiliasi delegasi saat startup mencegah transisi status tugas yang tidak valid." + "highlight1": "Provider Friendli dengan dukungan GLM-5.2 — gunakan model GLM terbaru lewat Friendli.", + "highlight2": "Dukungan thinking/reasoning native untuk Ollama — pertahankan keluaran penalaran dari awal hingga akhir saat kamu memakai model Ollama.", + "highlight3": "Perbaikan `apiModelId` kustom Anthropic — ID model Anthropic kustom kini tetap terpilih alih-alih diam-diam kembali ke `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Baru di Cloud:", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 398f08d527..caa84b54c0 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -341,9 +341,9 @@ "support": "Sostieni Zoo Code lasciandoci una stella su GitHub.", "release": { "heading": "Novità:", - "highlight1": "Supporto per Claude Sonnet 5 — l'ultimo modello di Claude è ora disponibile sui provider Anthropic, Bedrock e Vertex.", - "highlight2": "Aggiornamento a Semble v0.4.1 — analisi dei risultati appiattita e messaggi di stato localizzati.", - "highlight3": "Guardia di transizione di stato del ciclo di vita delle attività — una nuova guardia di transizione di stato e la riconciliazione della delega all'avvio prevengono transizioni di stato delle attività non valide." + "highlight1": "Provider Friendli con supporto GLM-5.2 — usa il modello GLM più recente tramite Friendli.", + "highlight2": "Supporto nativo thinking/reasoning per Ollama — conserva l'output di ragionamento end-to-end quando usi i modelli Ollama.", + "highlight3": "Correzione del `apiModelId` personalizzato di Anthropic — gli ID modello Anthropic personalizzati ora restano selezionati invece di tornare silenziosamente a `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Novità nel Cloud:", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 18c17967f1..9a28dc9f37 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -341,9 +341,9 @@ "support": "GitHubでスターを付けて Zoo Code を応援してください。", "release": { "heading": "新機能:", - "highlight1": "Claude Sonnet 5 サポート — 最新の Claude モデルが Anthropic、Bedrock、Vertex の各プロバイダーで利用可能になりました。", - "highlight2": "Semble v0.4.1 へのアップグレード — 平坦な結果解析とローカライズされたステータスメッセージ。", - "highlight3": "タスクライフサイクル状態遷移ガード — 新しい状態遷移ガードと起動時の委任調整により、無効なタスク状態遷移を防ぎます。" + "highlight1": "GLM-5.2 をサポートした Friendli プロバイダー — Friendli 経由で最新の GLM モデルを使えます。", + "highlight2": "Ollama のネイティブ thinking/reasoning サポート — Ollama モデル使用時に reasoning 出力をエンドツーエンドで保持します。", + "highlight3": "Anthropic のカスタム `apiModelId` 修正 — カスタム Anthropic モデル ID が `claude-sonnet-4-5` に黙ってフォールバックせず、そのまま選択された状態を維持します。" }, "cloudAgents": { "heading": "クラウドの新機能:", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 1a635aad7c..56b03165a7 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -341,9 +341,9 @@ "support": "GitHub에서 별표를 눌러 Zoo Code를 응원해 주세요.", "release": { "heading": "새로운 기능:", - "highlight1": "Claude Sonnet 5 지원 — 최신 Claude 모델이 이제 Anthropic, Bedrock, Vertex 프로바이더에서 사용 가능합니다.", - "highlight2": "Semble v0.4.1 업그레이드 — 평면화된 결과 파싱과 현지화된 상태 메시지.", - "highlight3": "태스크 수명주기 상태 전환 가드 — 새로운 상태 전환 가드와 시작 시 위임 조정으로 잘못된 태스크 상태 전환을 방지합니다." + "highlight1": "GLM-5.2를 지원하는 Friendli 프로바이더 — Friendli를 통해 최신 GLM 모델을 사용할 수 있습니다.", + "highlight2": "Ollama용 네이티브 thinking/reasoning 지원 — Ollama 모델을 사용할 때 reasoning 출력을 처음부터 끝까지 유지합니다.", + "highlight3": "Anthropic 사용자 지정 `apiModelId` 수정 — 사용자 지정 Anthropic 모델 ID가 이제 조용히 `claude-sonnet-4-5`로 되돌아가지 않고 선택된 상태로 유지됩니다." }, "cloudAgents": { "heading": "클라우드의 새로운 기능:", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 5c071449d4..357f5ca676 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -314,9 +314,9 @@ "support": "Steun Zoo Code door ons een ster te geven op GitHub.", "release": { "heading": "Wat is nieuw:", - "highlight1": "Claude Sonnet 5 ondersteuning — het nieuwste Claude-model is nu beschikbaar bij de Anthropic, Bedrock en Vertex providers.", - "highlight2": "Semble v0.4.1 upgrade — afgevlakte resultaatverwerking en gelokaliseerde statusberichten.", - "highlight3": "Taak-levenscyclus statusovergangsbewaking — een nieuwe statusovergangsbewaking en opstart-delegatieverzoening voorkomen ongeldige taakstatusovergangen." + "highlight1": "Friendli-provider met GLM-5.2-ondersteuning — gebruik het nieuwste GLM-model via Friendli.", + "highlight2": "Native thinking/reasoning-ondersteuning voor Ollama — behoud reasoning-uitvoer van begin tot eind wanneer je Ollama-modellen gebruikt.", + "highlight3": "Fix voor aangepaste Anthropic-`apiModelId` — aangepaste Anthropic-model-ID's blijven nu geselecteerd in plaats van stilletjes terug te vallen op `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Nieuw in de Cloud:", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 287808d3f5..0a04fe70ce 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -341,9 +341,9 @@ "support": "Wesprzyj Zoo Code, dając nam gwiazdkę na GitHub.", "release": { "heading": "Co nowego:", - "highlight1": "Obsługa Claude Sonnet 5 — najnowszy model Claude jest teraz dostępny u dostawców Anthropic, Bedrock i Vertex.", - "highlight2": "Aktualizacja do Semble v0.4.1 — spłaszczony parsing wyników i zlokalizowane komunikaty stanu.", - "highlight3": "Strażnik przejścia stanu cyklu życia zadań — nowy strażnik przejścia stanu i uzgadnianie delegacji przy uruchomieniu zapobiegają nieprawidłowym przejściom stanu zadań." + "highlight1": "Provider Friendli z obsługą GLM-5.2 — używaj najnowszego modelu GLM przez Friendli.", + "highlight2": "Natywna obsługa thinking/reasoning dla Ollamy — zachowuj wynik rozumowania od początku do końca podczas korzystania z modeli Ollama.", + "highlight3": "Poprawka niestandardowego `apiModelId` Anthropic — niestandardowe identyfikatory modeli Anthropic pozostają teraz wybrane zamiast po cichu wracać do `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Nowości w chmurze:", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index b50e4e2610..2673c2aba0 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -341,9 +341,9 @@ "support": "Apoie o Zoo Code nos dando uma estrela no GitHub.", "release": { "heading": "Novidades:", - "highlight1": "Suporte ao Claude Sonnet 5 — o modelo mais recente do Claude agora está disponível nos provedores Anthropic, Bedrock e Vertex.", - "highlight2": "Atualização para Semble v0.4.1 — análise de resultados achatada e mensagens de status localizadas.", - "highlight3": "Guarda de transição de estado do ciclo de vida das tarefas — uma nova guarda de transição de estado e a reconciliação de delegação na inicialização evitam transições de estado de tarefa inválidas." + "highlight1": "Provedor Friendli com suporte a GLM-5.2 — use o modelo GLM mais recente pelo Friendli.", + "highlight2": "Suporte nativo de thinking/reasoning para Ollama — preserve a saída de raciocínio de ponta a ponta ao usar modelos do Ollama.", + "highlight3": "Correção de `apiModelId` personalizado da Anthropic — os IDs de modelo personalizados da Anthropic agora permanecem selecionados em vez de voltar silenciosamente para `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Novidades na Nuvem:", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 55aec60136..74c69b2907 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -315,9 +315,9 @@ "support": "Поддержите Zoo Code, поставив нам звезду на GitHub.", "release": { "heading": "Что нового:", - "highlight1": "Поддержка Claude Sonnet 5 — новейшая модель Claude теперь доступна у провайдеров Anthropic, Bedrock и Vertex.", - "highlight2": "Обновление до Semble v0.4.1 — плоский разбор результатов и локализованные сообщения о статусе.", - "highlight3": "Страж перехода состояния жизненного цикла задач — новый страж перехода состояния и согласование делегирования при запуске предотвращают недопустимые переходы состояния задач." + "highlight1": "Провайдер Friendli с поддержкой GLM-5.2 — используй новейшую модель GLM через Friendli.", + "highlight2": "Нативная поддержка thinking/reasoning для Ollama — сохраняй вывод рассуждений от начала до конца при использовании моделей Ollama.", + "highlight3": "Исправление пользовательского `apiModelId` Anthropic — пользовательские идентификаторы моделей Anthropic теперь остаются выбранными вместо тихого возврата к `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Новое в облаке:", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 789fd94d7e..b1f0a248e2 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -342,9 +342,9 @@ "support": "Lütfen GitHub'da bize yıldız vererek Zoo Code'u destekle.", "release": { "heading": "Yenilikler:", - "highlight1": "Claude Sonnet 5 desteği — en yeni Claude modeli artık Anthropic, Bedrock ve Vertex sağlayıcılarında kullanılabilir.", - "highlight2": "Semble v0.4.1 yükseltmesi — düzleştirilmiş sonuç ayrıştırma ve yerelleştirilmiş durum mesajları.", - "highlight3": "Görev yaşam döngüsü durum geçiş koruması — yeni bir durum geçiş koruması ve başlangıç yetkilendirme uzlaştırması geçersiz görev durum geçişlerini önler." + "highlight1": "GLM-5.2 destekli Friendli sağlayıcısı — en yeni GLM modelini Friendli üzerinden kullan.", + "highlight2": "Ollama için yerel thinking/reasoning desteği — Ollama modellerini kullanırken reasoning çıktısını uçtan uca koru.", + "highlight3": "Anthropic özel `apiModelId` düzeltmesi — özel Anthropic model kimlikleri artık sessizce `claude-sonnet-4-5` modeline geri dönmek yerine seçili kalıyor." }, "cloudAgents": { "heading": "Cloud'daki yenilikler:", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index c0b730e4a0..b9a0a36b4f 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -342,9 +342,9 @@ "support": "Hãy ủng hộ Zoo Code bằng cách tặng sao cho chúng tôi trên GitHub.", "release": { "heading": "Có gì mới:", - "highlight1": "Hỗ trợ Claude Sonnet 5 — mô hình Claude mới nhất hiện có sẵn trên các nhà cung cấp Anthropic, Bedrock và Vertex.", - "highlight2": "Nâng cấp lên Semble v0.4.1 — phân tích kết quả làm phẳng và thông điệp trạng thái đã địa phương hóa.", - "highlight3": "Bảo vệ chuyển đổi trạng thái vòng đời tác vụ — một bảo vệ chuyển đổi trạng thái mới và đối chiếu ủy quyền khi khởi động ngăn chặn các chuyển đổi trạng thái tác vụ không hợp lệ." + "highlight1": "Nhà cung cấp Friendli với hỗ trợ GLM-5.2 — dùng mẫu GLM mới nhất qua Friendli.", + "highlight2": "Hỗ trợ thinking/reasoning gốc cho Ollama — giữ đầu ra suy luận xuyên suốt khi bạn dùng các mẫu Ollama.", + "highlight3": "Sửa lỗi `apiModelId` tùy chỉnh của Anthropic — ID mẫu Anthropic tùy chỉnh giờ sẽ tiếp tục được chọn thay vì âm thầm rơi về `claude-sonnet-4-5`." }, "cloudAgents": { "heading": "Mới trên Cloud:", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 70af4cb766..504662b4d3 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -342,9 +342,9 @@ "support": "请在 GitHub 上为 Zoo Code 点星支持我们。", "release": { "heading": "新增功能:", - "highlight1": "Claude Sonnet 5 支持 — 最新 Claude 模型现已在 Anthropic、Bedrock 和 Vertex 提供商上可用。", - "highlight2": "Semble v0.4.1 升级 — 扁平化的结果解析和本地化的状态消息。", - "highlight3": "任务生命周期状态转换守卫 — 新的状态转换守卫和启动时的委派对账可防止无效的任务状态转换。" + "highlight1": "支持 GLM-5.2 的 Friendli 提供商 — 通过 Friendli 使用最新的 GLM 模型。", + "highlight2": "Ollama 原生 thinking/reasoning 支持 — 使用 Ollama 模型时可端到端保留 reasoning 输出。", + "highlight3": "Anthropic 自定义 `apiModelId` 修复 — 自定义 Anthropic 模型 ID 现在会保持选中,不再静默回退到 `claude-sonnet-4-5`。" }, "cloudAgents": { "heading": "云端新功能:", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 9c41d02b16..9d31c51e9f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -363,9 +363,9 @@ "support": "請在 GitHub 上為 Zoo Code 加星支持我們。", "release": { "heading": "新增功能:", - "highlight1": "Claude Sonnet 5 支援 — 最新 Claude 模型現已於 Anthropic、Bedrock 和 Vertex 供應商可用。", - "highlight2": "Semble v0.4.1 升級 — 扁平化的結果解析和本地化的狀態訊息。", - "highlight3": "任務生命週期狀態轉換守衛 — 新的狀態轉換守衛和啟動時的委派對帳可防止無效的任務狀態轉換。" + "highlight1": "支援 GLM-5.2 的 Friendli 供應商 — 透過 Friendli 使用最新的 GLM 模型。", + "highlight2": "Ollama 原生 thinking/reasoning 支援 — 使用 Ollama 模型時可端對端保留 reasoning 輸出。", + "highlight3": "Anthropic 自訂 `apiModelId` 修正 — 自訂 Anthropic 模型 ID 現在會保持選取,不再靜默回退到 `claude-sonnet-4-5`。" }, "cloudAgents": { "heading": "雲端的新功能:", From 61f0b7f5fe00a0758ac4e41da95b50c44697b0c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:33:19 -0400 Subject: [PATCH 02/89] chore(deps): update dependency json-stream-stringify to v3.1.7 (#807) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b951c5c107..2fbdbe36fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -519,7 +519,7 @@ importers: version: 5.0.7 json-stream-stringify: specifier: ^3.1.6 - version: 3.1.6 + version: 3.1.7 lodash.debounce: specifier: ^4.0.8 version: 4.0.8 @@ -5764,8 +5764,8 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stream-stringify@3.1.6: - resolution: {integrity: sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==} + json-stream-stringify@3.1.7: + resolution: {integrity: sha512-F4MWetLtY42YMaAKw5cV4e47zMD5aOT+tjjQWjX18ACtdkQ5Y/vrcfbcQ107Rh+MXjOCIx4KhW0wPmOvG8iQ5w==} engines: {node: '>=7.10.1'} json-stringify-safe@5.0.1: @@ -14083,7 +14083,7 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stream-stringify@3.1.6: {} + json-stream-stringify@3.1.7: {} json-stringify-safe@5.0.1: {} From 5751a1690e61c7511ceedd5863c6fd40d2250e11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:39:22 -0400 Subject: [PATCH 03/89] chore(deps): update dependency reconnecting-eventsource to v1.6.5 (#808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fbdbe36fb..59159d8723 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -564,7 +564,7 @@ importers: version: 1.2.0 reconnecting-eventsource: specifier: ^1.6.4 - version: 1.6.4 + version: 1.6.5 safe-stable-stringify: specifier: ^2.5.0 version: 2.5.0 @@ -7208,8 +7208,8 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - reconnecting-eventsource@1.6.4: - resolution: {integrity: sha512-0L3IS3wxcNFApTPPHkcbY8Aya7XZIpYDzhxa8j6QSufVkUN018XJKfh2ZaThLBGP/iN5UTz2yweMhkqr0PKa7A==} + reconnecting-eventsource@1.6.5: + resolution: {integrity: sha512-L7ADnbp7yA42vvjwx7cZFNmiyHNBu/UldL1NCEvB23xBkZhESF83s2cnD9sLfvcX9wMIzW1UpIS7EOq/iu3E2g==} engines: {node: '>=12.0.0'} redent@3.0.0: @@ -15841,7 +15841,7 @@ snapshots: readdirp@4.1.2: {} - reconnecting-eventsource@1.6.4: {} + reconnecting-eventsource@1.6.5: {} redent@3.0.0: dependencies: From 01c1c99eb020b5a53a3baff785bf62de88bc0da2 Mon Sep 17 00:00:00 2001 From: Naved Merchant Date: Fri, 10 Jul 2026 19:49:07 -0700 Subject: [PATCH 04/89] feat(openai): add GPT-5.6 family (sol, terra, luna) support (#876) * feat(openai): add GPT-5.6 family (sol, terra, luna) support Add the GPT-5.6 model family to both the OpenAI Codex (ChatGPT subscription) and OpenAI Native (API) providers: - gpt-5.6-sol: flagship frontier reasoning/coding model - gpt-5.6-terra: balanced everyday model - gpt-5.6-luna: fastest, most affordable family member Update the default model id for both providers to gpt-5.6-sol and update the corresponding unit test assertions. Closes #871 * fix(openai-codex): correct GPT-5.6 context windows to 372000 Address review feedback on PR #876: the GPT-5.6 model context windows in the Codex provider were set to 400000, but the OpenAI codex models.json shows context_window and max_context_window of 372000 for all three GPT-5.6 variants (sol, terra, luna). Updated accordingly. --- packages/types/src/providers/openai-codex.ts | 52 ++++++++++++- packages/types/src/providers/openai.ts | 74 ++++++++++++++++++- .../providers/__tests__/openai-codex.spec.ts | 2 +- .../providers/__tests__/openai-native.spec.ts | 2 +- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/packages/types/src/providers/openai-codex.ts b/packages/types/src/providers/openai-codex.ts index fc80db3893..2caf00cd09 100644 --- a/packages/types/src/providers/openai-codex.ts +++ b/packages/types/src/providers/openai-codex.ts @@ -16,7 +16,7 @@ import type { ModelInfo } from "../model.js" export type OpenAiCodexModelId = keyof typeof openAiCodexModels -export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.3-codex" +export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.6-sol" /** * Models available through the Codex OAuth flow. @@ -24,6 +24,56 @@ export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.3-codex" * Costs are 0 as they are covered by the subscription. */ export const openAiCodexModels = { + "gpt-5.6-sol": { + maxTokens: 128000, + contextWindow: 372000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsVerbosity: true, + supportsTemperature: false, + description: + "GPT-5.6 Sol: OpenAI's flagship model for frontier reasoning, coding, and agentic workflows via ChatGPT subscription", + }, + "gpt-5.6-terra": { + maxTokens: 128000, + contextWindow: 372000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsVerbosity: true, + supportsTemperature: false, + description: + "GPT-5.6 Terra: Balanced everyday model with GPT-5.5-competitive performance via ChatGPT subscription", + }, + "gpt-5.6-luna": { + maxTokens: 128000, + contextWindow: 372000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsVerbosity: true, + supportsTemperature: false, + description: "GPT-5.6 Luna: The fastest, most affordable member of the GPT-5.6 family via ChatGPT subscription", + }, "gpt-5.1-codex-max": { maxTokens: 128000, contextWindow: 400000, diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 4f04af0a63..1601961964 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -3,9 +3,81 @@ import type { ModelInfo } from "../model.js" // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels -export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.1-codex-max" +export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.6-sol" export const openAiNativeModels = { + "gpt-5.6-sol": { + maxTokens: 128000, + contextWindow: 1_050_000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 5.0, + outputPrice: 30.0, + cacheWritesPrice: 6.25, + cacheReadsPrice: 0.5, + longContextPricing: { + thresholdTokens: 272_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 1.5, + appliesToServiceTiers: ["default", "flex"], + }, + supportsVerbosity: true, + supportsTemperature: false, + tiers: [ + { name: "flex", contextWindow: 1_050_000, inputPrice: 2.5, outputPrice: 15.0, cacheReadsPrice: 0.25 }, + { name: "priority", contextWindow: 1_050_000, inputPrice: 12.5, outputPrice: 75.0, cacheReadsPrice: 1.25 }, + ], + description: "GPT-5.6 Sol: OpenAI's flagship model for frontier reasoning, coding, and agentic workflows", + }, + "gpt-5.6-terra": { + maxTokens: 128000, + contextWindow: 1_050_000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 2.5, + outputPrice: 15.0, + cacheWritesPrice: 3.125, + cacheReadsPrice: 0.25, + longContextPricing: { + thresholdTokens: 272_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 1.5, + appliesToServiceTiers: ["default", "flex"], + }, + supportsVerbosity: true, + supportsTemperature: false, + tiers: [ + { name: "flex", contextWindow: 1_050_000, inputPrice: 1.25, outputPrice: 7.5, cacheReadsPrice: 0.125 }, + { name: "priority", contextWindow: 1_050_000, inputPrice: 6.25, outputPrice: 37.5, cacheReadsPrice: 0.625 }, + ], + description: "GPT-5.6 Terra: Balanced everyday model with GPT-5.5-competitive performance at 2x lower cost", + }, + "gpt-5.6-luna": { + maxTokens: 128000, + contextWindow: 400000, + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 1.0, + outputPrice: 6.0, + cacheWritesPrice: 1.25, + cacheReadsPrice: 0.1, + supportsVerbosity: true, + supportsTemperature: false, + tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.5, outputPrice: 3.0, cacheReadsPrice: 0.05 }], + description: "GPT-5.6 Luna: The fastest, most affordable member of the GPT-5.6 family", + }, "gpt-5.1-codex-max": { maxTokens: 128000, contextWindow: 400000, diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index 4ab9755252..22466af3ce 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -22,7 +22,7 @@ describe("OpenAiCodexHandler.getModel", () => { const handler = new OpenAiCodexHandler({ apiModelId: "not-a-real-model" }) const model = handler.getModel() - expect(model.id).toBe("gpt-5.3-codex") + expect(model.id).toBe("gpt-5.6-sol") expect(model.info).toBeDefined() }) diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 2a3ec0afb2..1acb4101be 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -356,7 +356,7 @@ describe("OpenAiNativeHandler", () => { openAiNativeApiKey: "test-api-key", }) const modelInfo = handlerWithoutModel.getModel() - expect(modelInfo.id).toBe("gpt-5.1-codex-max") // Default model + expect(modelInfo.id).toBe("gpt-5.6-sol") // Default model expect(modelInfo.info).toBeDefined() }) }) From 4c9fd5b447346729d48718867c59e62d9c599f88 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:55:54 +0000 Subject: [PATCH 05/89] chore(deps): update dependency remove-markdown to v0.6.4 (#809) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59159d8723..4fafda1e3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -847,7 +847,7 @@ importers: version: 6.0.0 remove-markdown: specifier: ^0.6.0 - version: 0.6.2 + version: 0.6.4 shell-quote: specifier: ^1.8.2 version: 1.8.4 @@ -7260,8 +7260,8 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - remove-markdown@0.6.2: - resolution: {integrity: sha512-EijDXJZbzpGbQBd852ViUzcqgpMujthM+SAEHiWCMcZonRbZ+xViWKLJA/vrwbDwYdxrs1aFDjpBhcGrZoJRGA==} + remove-markdown@0.6.4: + resolution: {integrity: sha512-BompiLClzjfh46irZmzv+1Q61jZYlKN3iq/hzae9EOUwf7ctr/5wpD4qwgn/FWDAYE18RORO7z/yr+HXZJCY8Q==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -15946,7 +15946,7 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remove-markdown@0.6.2: {} + remove-markdown@0.6.4: {} require-directory@2.1.1: {} From f07ae7380748a18e2b05132258430aa6356a49ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:08:19 -0400 Subject: [PATCH 06/89] chore(deps): update dependency source-map to v0.7.6 (#811) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fafda1e3d..ffd361da63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -856,7 +856,7 @@ importers: version: 3.4.1 source-map: specifier: ^0.7.4 - version: 0.7.4 + version: 0.7.6 stacktrace-js: specifier: ^2.0.2 version: 2.0.2 @@ -7579,9 +7579,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} @@ -16343,7 +16343,7 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} source-map@0.8.0-beta.0: dependencies: From 0d8c6fbd47637e00fb08926e7a92ed8e9538d832 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:49:57 +0000 Subject: [PATCH 07/89] chore(deps): update dependency sanitize-filename to v1.6.4 (#810) * chore(deps): update dependency sanitize-filename to v1.6.4 * test: fix flaky change detection test by wrapping rerender with act --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- pnpm-lock.yaml | 8 +-- .../SettingsView.change-detection.spec.tsx | 60 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffd361da63..037e6ba766 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -570,7 +570,7 @@ importers: version: 2.5.0 sanitize-filename: specifier: ^1.6.3 - version: 1.6.3 + version: 1.6.4 say: specifier: ^0.16.0 version: 0.16.0 @@ -7384,8 +7384,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-filename@1.6.3: - resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -16112,7 +16112,7 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-filename@1.6.3: + sanitize-filename@1.6.4: dependencies: truncate-utf8-bytes: 1.0.2 diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index 6effa4a8f8..2ca7107335 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -436,22 +436,25 @@ describe("SettingsView - Change Detection Fix", () => { fireEvent.click(screen.getByTestId("set-provider-deepseek")) expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek") - extensionState = createExtensionState({ - settingsImportedAt: 123, - soundEnabled: true, - apiConfiguration: { - apiProvider: "baseten", - apiModelId: "zai-org/GLM-4.6", - basetenApiKey: "test-baseten-key", - }, + await act(async () => { + extensionState = createExtensionState({ + settingsImportedAt: 123, + soundEnabled: true, + apiConfiguration: { + apiProvider: "baseten", + apiModelId: "zai-org/GLM-4.6", + basetenApiKey: "test-baseten-key", + }, + }) + ;(useExtensionState as any).mockImplementation(() => extensionState) + + rerender( + + + , + ) }) - rerender( - - - , - ) - // Let the import cache-busting effect run. With the old implementation, // this would reset cachedState back to the replayed Baseten config. await act(async () => { @@ -497,21 +500,24 @@ describe("SettingsView - Change Detection Fix", () => { fireEvent.click(screen.getByTestId("set-provider-deepseek")) expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek") - extensionState = createExtensionState({ - settingsImportedAt: 101, - apiConfiguration: { - apiProvider: "baseten", - apiModelId: "zai-org/GLM-4.6", - basetenApiKey: "imported-baseten-key", - }, + await act(async () => { + extensionState = createExtensionState({ + settingsImportedAt: 101, + apiConfiguration: { + apiProvider: "baseten", + apiModelId: "zai-org/GLM-4.6", + basetenApiKey: "imported-baseten-key", + }, + }) + ;(useExtensionState as any).mockImplementation(() => extensionState) + + rerender( + + + , + ) }) - rerender( - - - , - ) - await waitFor(() => { expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten") }) From a441e02bddd1a8de7b4ad4a846d14fcb8dd92cbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:57:02 +0000 Subject: [PATCH 08/89] chore(deps): update dependency tree-sitter-wasms to v0.1.13 (#812) * chore(deps): update dependency tree-sitter-wasms to v0.1.13 * test: add tree-sitter-dart.wasm to verified dist assets list --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- pnpm-lock.yaml | 8 ++++---- src/__tests__/dist_assets.spec.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 037e6ba766..3e7ecdc7b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -594,7 +594,7 @@ importers: version: 1.0.22 tree-sitter-wasms: specifier: ^0.1.12 - version: 0.1.12 + version: 0.1.13 undici: specifier: 6.27.0 version: 6.27.0 @@ -7957,8 +7957,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - tree-sitter-wasms@0.1.12: - resolution: {integrity: sha512-N9Jp+dkB23Ul5Gw0utm+3pvG4km4Fxsi2jmtMFg7ivzwqWPlSyrYQIrOmcX+79taVfcHEA+NzP0hl7vXL8DNUQ==} + tree-sitter-wasms@0.1.13: + resolution: {integrity: sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==} trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -16749,7 +16749,7 @@ snapshots: tree-kill@1.2.2: {} - tree-sitter-wasms@0.1.12: {} + tree-sitter-wasms@0.1.13: {} trim-lines@3.0.1: {} diff --git a/src/__tests__/dist_assets.spec.ts b/src/__tests__/dist_assets.spec.ts index 0177216c3e..bbd669bdce 100644 --- a/src/__tests__/dist_assets.spec.ts +++ b/src/__tests__/dist_assets.spec.ts @@ -21,6 +21,7 @@ describe("dist assets", () => { "tree-sitter-c_sharp.wasm", "tree-sitter-css.wasm", "tree-sitter-c.wasm", + "tree-sitter-dart.wasm", "tree-sitter-elisp.wasm", "tree-sitter-elixir.wasm", "tree-sitter-elm.wasm", From 9e218a5d010843d141e07866d2cbc14e57214a7b Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <263205322+roomote[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:00:40 +0000 Subject: [PATCH 09/89] fix(ci): reconcile labels against latest check run per name (#270) Co-authored-by: Elliott de Launay --- .github/workflows/label-pr-review-state.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/label-pr-review-state.yml b/.github/workflows/label-pr-review-state.yml index d79b77bc70..040ad91d00 100644 --- a/.github/workflows/label-pr-review-state.yml +++ b/.github/workflows/label-pr-review-state.yml @@ -153,9 +153,28 @@ jobs: }), ]); + // listForRef returns every check run ever recorded on the ref, including + // stale superseded ones (e.g. a failed run later re-run green). Branch + // protection and the PR UI only consider the latest run per check name, so + // reduce to that before evaluating — otherwise a single stale failure makes + // ciFailed true forever and state labels never come back. See issue #884. + // + // Unlike listReviews (which documents oldest-first order), listForRef's + // ordering is unspecified, so we pick the latest by run.id — GitHub assigns + // monotonically increasing IDs, and id is never null (a freshly re-queued + // run can have started_at: null, which would lose a string comparison + // against an older completed run's timestamp). + const latestByName = new Map(); + for (const run of checkRuns) { + const prev = latestByName.get(run.name); + if (!prev || run.id > prev.id) { + latestByName.set(run.name, run); + } + } + // Filter to required checks only (or all checks if rules unavailable). // Always exclude this workflow's own run to avoid self-referential loops. - const relevantRuns = checkRuns.filter(run => { + const relevantRuns = [...latestByName.values()].filter(run => { if (run.name === 'Reconcile PR review state labels') return false; return requiredCheckNames ? requiredCheckNames.has(run.name) : true; }); From 7d0879ba934067c9864c926d24c910049949fd49 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:33:36 -0400 Subject: [PATCH 10/89] fix(terminal): fix output loss on cold terminals by moving read() into onDidStartTerminalShellExecution (#800) (#834) * fix(terminal): detect D marker directly to recover from lost VSCode shell-integration completion signals * fix(terminal): move execution.read() into onDidStartTerminalShellExecution to fix cold-terminal zero-chunk output loss * fix(terminal): update unit tests for read()-in-TerminalRegistry architecture * fix(terminal): fix Windows CI test timing and improve coverage * fix(terminal): address code review findings * fix(terminal): address CodeRabbit review findings --- apps/vscode-e2e/fixtures/cold-shell-init.json | 18 + .../fixtures/fast-exit-shell-race.json | 18 + .../fixtures/long-running-silent-command.json | 18 + .../fixtures/terminal-reuse-shell-race.json | 18 + .../fixtures/zero-chunk-shell-race.json | 18 + .../src/fixtures/cold-shell-init.ts | 56 +++ .../src/fixtures/fast-exit-shell-race.ts | 32 ++ .../fixtures/long-running-silent-command.ts | 32 ++ .../src/fixtures/terminal-reuse-shell-race.ts | 42 ++ .../src/fixtures/zero-chunk-shell-race.ts | 25 + apps/vscode-e2e/src/runTest.ts | 10 + .../src/suite/tools/cold-shell-init.test.ts | 107 ++++ .../suite/tools/fast-exit-shell-race.test.ts | 116 +++++ .../tools/long-running-silent-command.test.ts | 111 +++++ .../tools/terminal-reuse-shell-race.test.ts | 94 ++++ .../suite/tools/zero-chunk-shell-race.test.ts | 103 ++++ src/__mocks__/vscode.js | 1 + src/core/tools/ExecuteCommandTool.ts | 50 +- .../__tests__/executeCommandTool.spec.ts | 16 +- src/integrations/terminal/Terminal.ts | 39 +- src/integrations/terminal/TerminalProcess.ts | 471 +++++++++++++++--- src/integrations/terminal/TerminalRegistry.ts | 58 ++- .../__tests__/TerminalProcess.spec.ts | 460 ++++++++++++++++- .../TerminalProcessExec.bash.spec.ts | 43 +- .../__tests__/TerminalProcessExec.cmd.spec.ts | 37 +- .../TerminalProcessExec.pwsh.spec.ts | 22 +- .../__tests__/TerminalProfile.spec.ts | 67 +++ .../__tests__/TerminalRegistry.spec.ts | 106 +++- 28 files changed, 2051 insertions(+), 137 deletions(-) create mode 100644 apps/vscode-e2e/fixtures/cold-shell-init.json create mode 100644 apps/vscode-e2e/fixtures/fast-exit-shell-race.json create mode 100644 apps/vscode-e2e/fixtures/long-running-silent-command.json create mode 100644 apps/vscode-e2e/fixtures/terminal-reuse-shell-race.json create mode 100644 apps/vscode-e2e/fixtures/zero-chunk-shell-race.json create mode 100644 apps/vscode-e2e/src/fixtures/cold-shell-init.ts create mode 100644 apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts create mode 100644 apps/vscode-e2e/src/fixtures/long-running-silent-command.ts create mode 100644 apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts create mode 100644 apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts create mode 100644 apps/vscode-e2e/src/suite/tools/cold-shell-init.test.ts create mode 100644 apps/vscode-e2e/src/suite/tools/fast-exit-shell-race.test.ts create mode 100644 apps/vscode-e2e/src/suite/tools/long-running-silent-command.test.ts create mode 100644 apps/vscode-e2e/src/suite/tools/terminal-reuse-shell-race.test.ts create mode 100644 apps/vscode-e2e/src/suite/tools/zero-chunk-shell-race.test.ts diff --git a/apps/vscode-e2e/fixtures/cold-shell-init.json b/apps/vscode-e2e/fixtures/cold-shell-init.json new file mode 100644 index 0000000000..68f5fc6a47 --- /dev/null +++ b/apps/vscode-e2e/fixtures/cold-shell-init.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "COLD_SHELL_INIT_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"python3 -c \\\"\\nimport sys\\nprint('cold-init-ok', file=sys.stdout)\\nsys.exit(0)\\n\\\"\"}", + "id": "call_cold_shell_init_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/fixtures/fast-exit-shell-race.json b/apps/vscode-e2e/fixtures/fast-exit-shell-race.json new file mode 100644 index 0000000000..52ee2d3e26 --- /dev/null +++ b/apps/vscode-e2e/fixtures/fast-exit-shell-race.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "FAST_EXIT_SHELL_RACE_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"python3 -c \\\"\\nimport sys\\nprint('boom', file=sys.stderr)\\nsys.exit(1)\\n\\\"\"}", + "id": "call_fast_exit_shell_race_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/fixtures/long-running-silent-command.json b/apps/vscode-e2e/fixtures/long-running-silent-command.json new file mode 100644 index 0000000000..dcc2f4a89a --- /dev/null +++ b/apps/vscode-e2e/fixtures/long-running-silent-command.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "LONG_RUNNING_SILENT_COMMAND_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"sleep 5\"}", + "id": "call_long_running_silent_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/fixtures/terminal-reuse-shell-race.json b/apps/vscode-e2e/fixtures/terminal-reuse-shell-race.json new file mode 100644 index 0000000000..7e938249bb --- /dev/null +++ b/apps/vscode-e2e/fixtures/terminal-reuse-shell-race.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "TERMINAL_REUSE_SHELL_RACE_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"python3 -c \\\"\\nimport sys\\nprint('first', file=sys.stderr)\\nsys.exit(0)\\n\\\"\"}", + "id": "call_terminal_reuse_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/fixtures/zero-chunk-shell-race.json b/apps/vscode-e2e/fixtures/zero-chunk-shell-race.json new file mode 100644 index 0000000000..211bb1c0bd --- /dev/null +++ b/apps/vscode-e2e/fixtures/zero-chunk-shell-race.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "ZERO_CHUNK_SHELL_RACE_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"python3 -c \\\"\\nimport sys\\nprint('boom', file=sys.stderr)\\nsys.exit(1)\\n\\\"\"}", + "id": "call_zero_chunk_shell_race_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/src/fixtures/cold-shell-init.ts b/apps/vscode-e2e/src/fixtures/cold-shell-init.ts new file mode 100644 index 0000000000..d5d25c6cdb --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/cold-shell-init.ts @@ -0,0 +1,56 @@ +import type { ChatCompletionRequest, ChatMessage } from "@copilotkit/aimock" + +import { LLMock } from "@copilotkit/aimock" + +function anyToolResultContains(req: ChatCompletionRequest, ...terms: string[]): boolean { + const messages: ChatMessage[] = Array.isArray(req?.messages) ? req.messages : [] + return messages.some( + (msg) => + msg?.role === "tool" && + typeof msg.content === "string" && + terms.every((t) => (msg.content as string).includes(t)), + ) +} + +export function addColdShellInitFixtures(mock: InstanceType) { + // On cold zsh terminals the first execution may produce 0 chunks (VSCode + // execution.read() limitation on basic shell integration — see issue #242897). + // When the first result is empty, the mock retries the same command so the + // second attempt (on the now-warm terminal) captures real output. + mock.addFixture({ + match: { + toolCallId: "call_cold_shell_init_001", + predicate: (req: ChatCompletionRequest) => + // First attempt returned empty — retry the command + !anyToolResultContains(req, "cold-init-ok"), + }, + response: { + toolCalls: [ + { + name: "execute_command", + arguments: JSON.stringify({ + command: "python3 -c \"\nimport sys\nprint('cold-init-ok', file=sys.stdout)\nsys.exit(0)\n\"", + }), + id: "call_cold_shell_init_003", + }, + ], + }, + }) + + // Match whichever attempt (first or retry) delivers real output — prove + // the guard kept the process alive long enough for the output to arrive. + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => anyToolResultContains(req, "cold-init-ok", "Exit code: 0"), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "Cold shell init command completed with real output." }), + id: "call_cold_shell_init_002", + }, + ], + }, + }) +} diff --git a/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts b/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts new file mode 100644 index 0000000000..a5a92d427b --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts @@ -0,0 +1,32 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./tool-result" + +export function addFastExitShellRaceResultFixtures(mock: InstanceType) { + mock.addFixture({ + match: { + toolCallId: "call_fast_exit_shell_race_001", + // VSCode drops onDidEndTerminalShellExecution for this command (the race under + // test), so TerminalProcess.run() only has the D marker itself as proof of + // completion, never a real exit code (see ExecuteCommandTool.ts's + // `exitDetails === undefined` branch). Match on the actual stderr output and the + // specific unknown-exit-status text so this fixture -- and the e2e assertions it + // drives -- would fail if either the output capture or that fallback wording + // regressed, instead of passing on any generic "command executed" result. + predicate: (req) => + toolResultContains(req, "call_fast_exit_shell_race_001", [ + "boom", + "", + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "The script ran and printed 'boom' to stderr." }), + id: "call_fast_exit_shell_race_002", + }, + ], + }, + }) +} diff --git a/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts b/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts new file mode 100644 index 0000000000..af9a757c09 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts @@ -0,0 +1,32 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./tool-result" + +export function addLongRuningSilentCommandFixtures(mock: InstanceType) { + // `sleep 5` produces no output and completes normally via onDidEndTerminalShellExecution. + // The idle timeout must NOT fire here — it must only fire on zero-chunk commands where + // the stream stays open AND the event is delayed (the { ... }-wrapped multiline bug). + // For `sleep 5`, the stream stays open (so the idle timer is never active after the + // first chunk — but actually sleep 5 may produce no chunks either). The distinction + // is that `sleep 5` DOES receive onDidEndTerminalShellExecution promptly after exit, + // which breaks the loop via DONE_SENTINEL before the 3s idle timer fires. + mock.addFixture({ + match: { + toolCallId: "call_long_running_silent_001", + predicate: (req) => + toolResultContains(req, "call_long_running_silent_001", [ + // sleep exits with code 0 — the normal exit status path + "Exit code: 0", + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "The sleep command completed successfully." }), + id: "call_long_running_silent_002", + }, + ], + }, + }) +} diff --git a/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts b/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts new file mode 100644 index 0000000000..01cae1c9f9 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts @@ -0,0 +1,42 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./tool-result" + +export function addTerminalReuseShellRaceFixtures(mock: InstanceType) { + // First command completes — model issues a second command on the same terminal. + // With the temp-script fix, both commands now deliver real output. + mock.addFixture({ + match: { + toolCallId: "call_terminal_reuse_001", + predicate: (req) => toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"]), + }, + response: { + toolCalls: [ + { + name: "execute_command", + arguments: JSON.stringify({ + command: "python3 -c \"\nimport sys\nprint('second', file=sys.stderr)\nsys.exit(0)\n\"", + }), + id: "call_terminal_reuse_002", + }, + ], + }, + }) + + // Second command on the reused terminal also completes. + mock.addFixture({ + match: { + toolCallId: "call_terminal_reuse_002", + predicate: (req) => toolResultContains(req, "call_terminal_reuse_002", ["second", "Exit code: 0"]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "Both commands ran on the reused terminal." }), + id: "call_terminal_reuse_003", + }, + ], + }, + }) +} diff --git a/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts b/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts new file mode 100644 index 0000000000..0cb0c591cf --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts @@ -0,0 +1,25 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./tool-result" + +export function addZeroChunkShellRaceResultFixtures(mock: InstanceType) { + mock.addFixture({ + match: { + toolCallId: "call_zero_chunk_shell_race_001", + // The multiline command is now written to a temp script file and executed + // via `sh /tmp/roo-cmd-*.sh` to avoid the VSCode { ... }-wrapping bug that + // caused the stream to be closed before read() arrived (zero chunks). + // The real output ('boom' on stderr) and exit code (1) now reach the model. + predicate: (req) => toolResultContains(req, "call_zero_chunk_shell_race_001", ["boom", "Exit code: 1"]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "The script ran and printed 'boom' to stderr." }), + id: "call_zero_chunk_shell_race_002", + }, + ], + }, + }) +} diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 23c7f317fe..92d772e5ad 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -8,6 +8,11 @@ import { LLMock } from "@copilotkit/aimock" import { addApplyDiffResultFixtures } from "./fixtures/apply-diff" import { addExecuteCommandResultFixtures } from "./fixtures/execute-command" +import { addFastExitShellRaceResultFixtures } from "./fixtures/fast-exit-shell-race" +import { addZeroChunkShellRaceResultFixtures } from "./fixtures/zero-chunk-shell-race" +import { addTerminalReuseShellRaceFixtures } from "./fixtures/terminal-reuse-shell-race" +import { addLongRuningSilentCommandFixtures } from "./fixtures/long-running-silent-command" +import { addColdShellInitFixtures } from "./fixtures/cold-shell-init" import { addTerminalProfileResultFixtures } from "./fixtures/terminal-profile" import { addListFilesResultFixtures } from "./fixtures/list-files" import { addReadFileResultFixtures } from "./fixtures/read-file" @@ -110,6 +115,11 @@ async function main() { if (!isRecord) { addApplyDiffResultFixtures(mock) addExecuteCommandResultFixtures(mock) + addFastExitShellRaceResultFixtures(mock) + addZeroChunkShellRaceResultFixtures(mock) + addTerminalReuseShellRaceFixtures(mock) + addLongRuningSilentCommandFixtures(mock) + addColdShellInitFixtures(mock) addTerminalProfileResultFixtures(mock) addListFilesResultFixtures(mock) addReadFileResultFixtures(mock) diff --git a/apps/vscode-e2e/src/suite/tools/cold-shell-init.test.ts b/apps/vscode-e2e/src/suite/tools/cold-shell-init.test.ts new file mode 100644 index 0000000000..a058fb82a4 --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/cold-shell-init.test.ts @@ -0,0 +1,107 @@ +/** + * Regression guard for the idle-timeout-during-shell-initialization race: + * on a cold terminal, onDidStartTerminalShellExecution can fire AFTER the + * 3-second idle window. Without the guard added in TerminalProcess.run(), + * the IDLE_SENTINEL path self-finalizes before the command starts executing, + * silently dropping its output. + * + * The fix: when IDLE_SENTINEL fires but the shell_execution_started event has + * not been received, continue waiting (re-arm the idle timer) up to + * Shell Integration Timeout ms. Only self-finalize once the event has arrived + * OR the full timeout is exhausted. + * + * NOTE on cold-zsh first-command zero-chunks: VSCode's execution.read() API + * has known reliability issues on "basic" shell integration (documented in + * https://github.com/microsoft/vscode/issues/242897). On a cold zsh terminal + * the very first command may produce zero stream chunks even though the + * command ran successfully — this is a VSCode API limitation, not a Zoo Code + * bug. The model typically retries and captures output on the second attempt + * (warm terminal). This test verifies the guard prevents a PREMATURE + * self-finalize and that the model eventually receives "cold-init-ok" output + * (either on the first or a retry attempt). + * + * See: https://github.com/Zoo-Code-Org/Zoo-Code/issues/800 + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +suite("Cold shell init — idle timeout must not misfire before shell starts", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("captures output from a multiline command on a fresh terminal", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + const startedAt = Date.now() + + try { + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "COLD_SHELL_INIT_E2E", + }), + timeout: 60_000, + }) + + const elapsedMs = Date.now() - startedAt + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + // The fixture only responds with attempt_completion once its predicate + // confirms "cold-init-ok" and "Exit code: 0" are in the tool result. + // If the idle timeout misfired (premature self-finalize), the output + // would be empty/unknown and the predicate would never match. + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + `Task should have reached attempt_completion with real output (elapsed: ${elapsedMs}ms). ` + + `If this timed out, the idle timeout may have misfired before onDidStartTerminalShellExecution fired.`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/fast-exit-shell-race.test.ts b/apps/vscode-e2e/src/suite/tools/fast-exit-shell-race.test.ts new file mode 100644 index 0000000000..befae6f24b --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/fast-exit-shell-race.test.ts @@ -0,0 +1,116 @@ +/** + * Regression test for a real VS Code shell-integration race: a fast-exiting, + * multi-line command (wrapped by prepareCommandForShellIntegration into a single + * `{ ... }` shell execution) can complete in the terminal while VS Code never + * delivers the completion signal -- onDidEndTerminalShellExecution never fires and + * the shellIntegration.executeCommand().read() stream never closes on its own, even + * though the ]633;D marker text IS written into the stream. + * + * TerminalProcess.run() recovers by detecting the D marker itself directly in the + * accumulated stream data (independent of the stream/event ever formally confirming + * completion), then waiting only a brief grace period for the real + * onDidEndTerminalShellExecution/exit-code event before proceeding without one. + * + * This is deliberately narrow: it only self-finalizes on positive proof (the marker + * text itself), never on a guessed "gone quiet, must be done" timeout -- a genuinely + * long-running, silent command (a cold `tsc --noEmit`, a build, etc.) is + * indistinguishable from lost-signal by elapsed time alone, so no such guess is made. + * If the marker itself is ever lost too, this still hangs, bounded only by the user's + * own commandExecutionTimeout / the model's agentTimeout at the tool layer. + * + * See: https://github.com/microsoft/vscode/issues/316556 + * https://github.com/microsoft/vscode/issues/250764 + * https://github.com/microsoft/vscode/issues/254724 + * + * This exercises the real VS Code integrated terminal (terminalShellIntegrationDisabled: + * false), not the Execa fallback, since the race lives specifically in VS Code's + * shell-integration event/stream plumbing. + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +suite("Fast-exit shell integration race", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("completes a fast-exiting multi-line command via the real VS Code terminal", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + const startedAt = Date.now() + + try { + // Bounded well under the un-fixed hang (which stalls for the full 60s test + // timeout with zero output). TerminalProcess.run() detects the D marker itself + // and only waits a brief grace period (~1s) for the real exit code afterward, so + // a healthy run finishes in well under 30s; a regression back to the old hang + // will blow this timeout. + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "FAST_EXIT_SHELL_RACE_E2E", + }), + timeout: 60_000, + }) + + const elapsedMs = Date.now() - startedAt + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + // The mock's fixture (see fast-exit-shell-race.ts) only responds with + // attempt_completion once its predicate has already verified the tool result + // contains the actual 'boom' stderr output AND the specific unknown-exit-status + // wording -- so reaching completion_result at all is itself proof that content + // survived the lost completion signal. + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + `Task should have reached attempt_completion instead of hanging on the command (elapsed: ${elapsedMs}ms)`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/long-running-silent-command.test.ts b/apps/vscode-e2e/src/suite/tools/long-running-silent-command.test.ts new file mode 100644 index 0000000000..1677e2dccd --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/long-running-silent-command.test.ts @@ -0,0 +1,111 @@ +/** + * Regression guard: the zero-chunk idle timeout must NOT fire on a legitimate + * long-running, silent command. + * + * The idle timeout in TerminalProcess.run() fires when no stream data arrives + * within 3s AND no onDidEndTerminalShellExecution event has fired. For the + * zero-chunk bug ({ ... }-wrapped multiline commands), that event is delayed + * 60+ seconds, so the 3s timeout is the only way to unblock. + * + * For a simple `sleep 5`, VSCode DOES fire onDidEndTerminalShellExecution + * promptly after the command exits — which breaks the iterator loop via the + * DONE_SENTINEL path before the 3s idle timer fires. The command should + * therefore complete with a real exit code (0), not the "exitDetails == undefined" + * sentinel that indicates the idle-timeout path was taken. + * + * If this test regresses (e.g. the idle timeout fires and returns exitDetails==undefined + * instead of exit code 0), it means the fix would falsely terminate long-running + * silent commands — the core concern raised when the original 15s idle timeout was reverted. + * + * See: https://github.com/Zoo-Code-Org/Zoo-Code/issues/800 + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +suite("Long-running silent command (idle timeout must not misfire)", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("completes a 5s silent command with exit code 0, not via idle timeout", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + const startedAt = Date.now() + + try { + // sleep 5 takes ~5s; allow plenty of headroom. + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "LONG_RUNNING_SILENT_COMMAND_E2E", + }), + timeout: 60_000, + }) + + const elapsedMs = Date.now() - startedAt + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + // The mock fixture only matches "Exit code: 0" — if the idle timeout fired + // instead we'd get "exitDetails == undefined" and the fixture wouldn't match, + // leaving the task waiting for an API response until the test times out. + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + `Task should have completed with exit code 0 (elapsed: ${elapsedMs}ms). ` + + `If this timed out, the idle timeout likely misfired on a legitimate quiet command.`, + ) + + // Sanity check: should take at least 4s (sleep 5), not 3s (idle timeout). + assert.ok( + elapsedMs >= 4_000, + `Expected elapsed >= 4000ms to confirm sleep 5 ran to completion, got ${elapsedMs}ms. ` + + `The idle timeout may have fired prematurely.`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/terminal-reuse-shell-race.test.ts b/apps/vscode-e2e/src/suite/tools/terminal-reuse-shell-race.test.ts new file mode 100644 index 0000000000..40b2d6e339 --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/terminal-reuse-shell-race.test.ts @@ -0,0 +1,94 @@ +/** + * Regression test: terminal reuse after a zero-chunk command. + * + * After the idle-timeout self-finalization path fires for a zero-chunk + * { ... }-wrapped multiline command, the task issues a second execute_command + * that reuses the same VS Code terminal. This verifies: + * + * 1. The stale-execution guard (ownExecution check) correctly ignores the + * late onDidEndTerminalShellExecution from the first command when the + * second command is already running on the reused terminal. + * 2. The second command also completes (via the same idle-timeout path), + * confirming the terminal is in a clean state after reuse. + * + * See: https://github.com/Zoo-Code-Org/Zoo-Code/issues/800 + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +suite("Terminal reuse after zero-chunk shell race", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("completes two sequential zero-chunk commands on a reused terminal", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + const startedAt = Date.now() + + try { + // Two commands, each with 3s idle timeout, plus overhead — 30s is comfortable. + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "TERMINAL_REUSE_SHELL_RACE_E2E", + }), + timeout: 60_000, + }) + + const elapsedMs = Date.now() - startedAt + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + `Task should have completed both commands and reached attempt_completion (elapsed: ${elapsedMs}ms)`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/zero-chunk-shell-race.test.ts b/apps/vscode-e2e/src/suite/tools/zero-chunk-shell-race.test.ts new file mode 100644 index 0000000000..cf82952230 --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/zero-chunk-shell-race.test.ts @@ -0,0 +1,103 @@ +/** + * Regression test for the zero-chunk VSCode shell-integration bug: a multiline + * command wrapped into `{ ... }` by prepareCommandForShellIntegration causes + * VSCode to suppress BOTH the stream data AND onDidEndTerminalShellExecution. + * The for-await loop exits immediately with chunkCount === 0, then the original + * code hung forever at `await shellExecutionComplete`. + * + * Fix: multiline commands are written to a temp script file and executed via + * `sh /tmp/roo-cmd-*.sh` instead of being wrapped in `{ ... }`. This avoids + * VSCode's multiline compound-command code path that closes the stream before + * read() is called. The real output and exit code now reach the model. + * + * This is distinct from the fast-exit-shell-race test, which covers the case + * where VSCode DOES emit stream data including the D marker but never fires + * onDidEndTerminalShellExecution. Here, without the fix, not even stream data arrives. + * + * See: https://github.com/Zoo-Code-Org/Zoo-Code/issues/800 + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +suite("Zero-chunk shell integration race", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("completes a zero-chunk multiline command via the real VS Code terminal", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + const startedAt = Date.now() + + try { + // The grace timer fires after 1s; the whole run should complete well under + // 30s. A regression back to the old hang blows the suite timeout (20m) or + // this explicit timeout, whichever comes first. + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "ZERO_CHUNK_SHELL_RACE_E2E", + }), + timeout: 60_000, + }) + + const elapsedMs = Date.now() - startedAt + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + // The mock fixture only responds with attempt_completion once its predicate + // confirms the tool result contains 'boom' and 'Exit code: 1' — proof that + // the temp-script fix caused the stream to deliver real output. + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + `Task should have reached attempt_completion with real output (elapsed: ${elapsedMs}ms). ` + + `If this timed out, the multiline command may still be hitting the zero-chunk VSCode bug.`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) +}) diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index 505497fc9f..8ace25aeb0 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -81,6 +81,7 @@ export const window = { sendText: () => {}, }), onDidCloseTerminal: () => mockDisposable, + onDidChangeTerminalShellIntegration: () => mockDisposable, createTextEditorDecorationType: () => ({ dispose: () => {} }), } diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index 58b0055b4c..3d6a44ef18 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -379,30 +379,38 @@ export async function executeCommandInTerminal( } }, onCompleted: async (output: string | undefined) => { - try { - clearTimeout(pendingCommandOutputEmitTimer) - pendingCommandOutputEmitTimer = undefined + clearTimeout(pendingCommandOutputEmitTimer) + pendingCommandOutputEmitTimer = undefined + try { // Finalize interceptor and get persisted result. // We await finalize() to ensure the artifact file is fully flushed // before we advertise the artifact_id to the LLM. if (interceptor) { persistedResult = await interceptor.finalize() } - - // Continue using compressed output for UI display - result = Terminal.compressTerminalOutput(output ?? "") - latestCompressedOutput = result - - // Preserve order: wait for queued partial updates, then emit the final - // non-partial command_output update. - await commandOutputSayChain - await queueCommandOutputMessage(result, false, true) - completed = true - } finally { - // Signal that onCompleted has finished, so the main code can safely use persistedResult - resolveOnCompleted?.() + } catch (error) { + console.error("[ExecuteCommandTool] interceptor.finalize() failed:", error) } + + // Continue using compressed output for UI display + result = Terminal.compressTerminalOutput(output ?? "") + latestCompressedOutput = result + completed = true + + // Unblock the main code path: persistedResult, result, and completed are + // all set now. Resolve before draining the UI say chain so that a stalled + // or slow webview update cannot prevent the tool result from being returned. + resolveOnCompleted?.() + + // Preserve order: wait for queued partial updates, then emit the final + // non-partial command_output update. Fire-and-forget from the main path — + // errors here are UI-only and must not surface to the tool result. + commandOutputSayChain + .then(() => queueCommandOutputMessage(result, false, true)) + .catch((error) => { + console.error("[ExecuteCommandTool] Failed to flush final command_output:", error) + }) }, onShellExecutionStarted: (pid: number | undefined) => { const status: CommandExecutionStatus = { executionId, status: "started", pid, command } @@ -508,10 +516,12 @@ export async function executeCommandInTerminal( // grouping command_output messages despite any gaps anyways). await delay(50) - // Wait for onCompleted callback to finish if shell execution completed. - // This ensures persistedResult is set before we try to use it, fixing the race - // condition where exitDetails is set (sync) before the async onCompleted finishes. - if (exitDetails && onCompletedPromise) { + // Wait for onCompleted callback to finish. onCompleted is async and sets + // `completed` and `persistedResult`; we must not read them until it resolves. + // Skip when returning a background result: the command is still running and + // onCompleted will fire later — awaiting it here would block until real completion, + // defeating the purpose of the agent-timeout background transition. + if (!runInBackground) { await onCompletedPromise } diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index d19906a262..1725154328 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -29,7 +29,13 @@ vitest.mock("vscode", () => ({ vitest.mock("../../../integrations/terminal/TerminalRegistry", () => ({ TerminalRegistry: { getOrCreateTerminal: vitest.fn().mockResolvedValue({ - runCommand: vitest.fn().mockResolvedValue(undefined), + runCommand: vitest.fn().mockImplementation((_cmd: string, callbacks: any) => { + // Invoke onCompleted so onCompletedPromise resolves and the tool returns. + callbacks?.onCompleted?.("") + const p = Promise.resolve() + // Attach promise-like properties so mergePromise callers don't throw. + return Object.assign(p, { continue: () => {}, abort: () => {} }) + }), getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/workspace"), }), }, @@ -181,12 +187,12 @@ describe("executeCommandTool", () => { pushToolResult: mockPushToolResult as unknown as PushToolResult, }) - // Verify - confirm the command was approved and result was pushed - // The custom path handling is tested in integration tests + // Verify - command approved, result pushed, and custom cwd passed to terminal expect(mockAskApproval).toHaveBeenCalledWith("command", "echo test") expect(mockPushToolResult).toHaveBeenCalled() - const result = mockPushToolResult.mock.calls[0][0] - expect(result).toContain("/custom/path") + const { TerminalRegistry } = await import("../../../integrations/terminal/TerminalRegistry") + const firstArg = (TerminalRegistry.getOrCreateTerminal as ReturnType).mock.calls[0][0] + expect(firstArg).toBe("/custom/path") }) }) diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 49dc347c52..675ffbf3e7 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -2,7 +2,6 @@ import { existsSync } from "fs" import * as path from "path" import * as vscode from "vscode" -import pWaitFor from "p-wait-for" import type { RooTerminalCallbacks, RooTerminalProcessResultPromise } from "./types" import { BaseTerminal } from "./BaseTerminal" @@ -117,10 +116,12 @@ export class Terminal extends BaseTerminal { commandSubmitted: false, }) } else { - // Wait for shell integration before executing the command - pWaitFor(() => this.terminal.shellIntegration !== undefined, { - timeout: Terminal.getShellIntegrationTimeout(), - }) + // Wait for shell integration to activate before executing the command. + // Use the onDidChangeTerminalShellIntegration event rather than polling + // so we react immediately when the shell is ready. The timeout is kept as + // a safety net for shells that never activate integration (e.g. heavily + // customised startup that suppresses the OSC 633;A marker). + this.waitForShellIntegration(Terminal.getShellIntegrationTimeout()) .then(() => { // Clean up temporary directory if shell integration is available, zsh did its job: ShellIntegrationManager.zshCleanupTmpDir(this.id) @@ -145,6 +146,34 @@ export class Terminal extends BaseTerminal { return mergePromise(process, promise) } + /** + * Resolves when this terminal's shell integration becomes active, or rejects + * after timeoutMs if the shell never signals readiness. Uses the + * onDidChangeTerminalShellIntegration event so we react immediately rather + * than polling — important for slow-starting shells (heavy .zshrc, nvm, etc.). + */ + private waitForShellIntegration(timeoutMs: number): Promise { + if (this.terminal.shellIntegration) { + return Promise.resolve() + } + + return new Promise((resolve, reject) => { + const ref = { disposable: null as vscode.Disposable | null } + const timer = setTimeout(() => { + ref.disposable?.dispose() + reject(new Error(`Shell integration did not activate within ${timeoutMs / 1000}s`)) + }, timeoutMs) + + ref.disposable = vscode.window.onDidChangeTerminalShellIntegration((e) => { + if (e.terminal === this.terminal) { + clearTimeout(timer) + ref.disposable?.dispose() + resolve() + } + }) + }) + } + /** * Gets the terminal contents based on the number of commands to include * @param commands Number of previous commands to include (-1 for all) diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts index 694c5ff340..d1643dec3a 100644 --- a/src/integrations/terminal/TerminalProcess.ts +++ b/src/integrations/terminal/TerminalProcess.ts @@ -1,3 +1,7 @@ +import * as fs from "fs" +import * as os from "os" +import * as path from "path" + import * as vscode from "vscode" import type { ExitCodeDetails } from "./types" @@ -19,6 +23,13 @@ export class TerminalProcess extends BaseTerminalProcess { // Guards against overlapping abort retry loops if abort() is called again // while a previous loop is still re-sending Ctrl+C. private aborting = false + // The specific VSCode shell execution this process was started with. Kept on the + // process (not just terminal.activeShellExecution, which gets reused/reassigned as + // soon as the next command starts) so TerminalRegistry can tell a late + // onDidEndTerminalShellExecution event for THIS execution apart from one belonging to + // whatever command is currently running on the same reused terminal -- see the + // self-finalize grace period in run()'s finalize(). + public ownExecution?: vscode.TerminalShellExecution constructor(terminal: Terminal) { super() @@ -75,7 +86,10 @@ export class TerminalProcess extends BaseTerminalProcess { return } - // Create a promise that resolves when the stream becomes available + // Create a promise that resolves when the stream becomes available. + // cancelStreamWait() lets the early-completion race path abort the pending + // timeout so it doesn't fire (and reject) after we've already returned. + let cancelStreamWait: () => void = () => {} const streamAvailable = new Promise>((resolve, reject) => { const timeoutId = setTimeout(() => { // Remove event listener to prevent memory leaks @@ -95,6 +109,11 @@ export class TerminalProcess extends BaseTerminalProcess { ) }, Terminal.getShellIntegrationTimeout()) + cancelStreamWait = () => { + clearTimeout(timeoutId) + this.removeAllListeners("stream_available") + } + // Clean up timeout if stream becomes available this.once("stream_available", (stream: AsyncIterable) => { clearTimeout(timeoutId) @@ -102,9 +121,28 @@ export class TerminalProcess extends BaseTerminalProcess { }) }) - // Create promise that resolves when shell execution completes for this terminal + // Create promise that resolves when shell execution completes for this terminal. + // We also expose a "done" sentinel so the stream-reading loop can be interrupted + // when this event fires while we are blocked awaiting the next chunk — VSCode's + // AsyncIterable has no built-in cancellation mechanism. + const doneSignal = { done: false } + // Resolves to a sentinel object (not ExitCodeDetails) so we can distinguish it + // from a real chunk in the interruptible loop below. const shellExecutionComplete = new Promise((resolve) => { - this.once("shell_execution_complete", (details: ExitCodeDetails) => resolve(details)) + this.once("shell_execution_complete", (details: ExitCodeDetails) => { + doneSignal.done = true + resolve(details) + }) + }) + + // Register shell_execution_started listener BEFORE awaiting streamAvailable. + // BaseTerminal.setActiveStream emits shell_execution_started and stream_available + // on the same synchronous tick (in that order). If we register after the await, + // we miss the event and shellExecutionStarted stays false, causing the idle-timeout + // guard to incorrectly treat a running-but-silent command as stalled. + let shellExecutionStarted = false + this.once("shell_execution_started", () => { + shellExecutionStarted = true }) // Execute command. @@ -136,24 +174,50 @@ export class TerminalProcess extends BaseTerminalProcess { this.prepareCommandForShellIntegration(commandToExecute, shellKind), ) + this.ownExecution = execution this.terminal.activeShellExecution = execution - - // VS Code only captures data written after read() is first called, so read - // the execution stream immediately instead of waiting for the global start - // event to deliver the same execution later. - this.terminal.setActiveStream(execution.read()) + // Do NOT call execution.read() here. Reading must happen inside + // onDidStartTerminalShellExecution (TerminalRegistry), which fires when + // VSCode's shell integration confirms the command has actually started + // executing. Calling read() before that event — particularly on a cold + // terminal where the shell is still initializing — creates a stream window + // that misses output: the execution begins after the stream was opened, + // VSCode doesn't buffer retroactively, and zero chunks arrive. } catch (error) { this.terminal.activeShellExecution = undefined + this.cleanupScriptFile() throw error } this.isHot = true - // Wait for stream to be available + // Wait for stream to be available, but also race against shellExecutionComplete. + // If the end event fires before the stream arrives (e.g. a zero-output command + // where onDidEndTerminalShellExecution beats onDidStartTerminalShellExecution), + // we would otherwise block until the stream timeout fires. Resolving early on + // completion produces a clean empty-output result instead. let stream: AsyncIterable try { - stream = await streamAvailable + const COMPLETED_BEFORE_STREAM = Symbol("completed_before_stream") + const result = await Promise.race([ + streamAvailable, + shellExecutionComplete.then(() => COMPLETED_BEFORE_STREAM as typeof COMPLETED_BEFORE_STREAM), + ]) + + if (result === COMPLETED_BEFORE_STREAM) { + console.info("[Terminal Process] shell execution completed before stream arrived — finishing cleanly") + cancelStreamWait() + this.terminal.activeShellExecution = undefined + this.terminal.busy = false + this.isHot = false + this.cleanupScriptFile() + this.emit("completed", "") + this.emit("continue") + return + } + + stream = result as AsyncIterable } catch (error) { // Stream timeout or other error occurred console.error("[Terminal Process] Stream error:", error.message) @@ -165,6 +229,7 @@ export class TerminalProcess extends BaseTerminalProcess { ) this.terminal.busy = false + this.cleanupScriptFile() // Emit continue event to allow execution to proceed this.emit("continue") @@ -181,68 +246,298 @@ export class TerminalProcess extends BaseTerminalProcess { * - OSC 633 ; E ; [; ] ST - Explicitly set command line with optional nonce */ - // Process stream data - for await (let data of stream) { - const match = this.fullOutput === "" ? this.matchAfterVsceStartMarkers(data) : undefined + // Process stream data. + // + // VSCode bug: on some platforms/shells (observed with multi-line commands and + // certain compound single-line commands), the stream can fail to close on its own + // and onDidEndTerminalShellExecution can fail to fire, even though the ]633;D end + // marker IS written into the stream -- the command visibly finished, but the event + // that would normally tell us so is silently dropped. See: + // https://github.com/microsoft/vscode/issues/316556, + // https://github.com/microsoft/vscode/issues/250764, + // https://github.com/microsoft/vscode/issues/254724. + // + // We can safely self-finalize on the D marker text alone: it is real, positive + // proof the shell finished, independent of whether the stream/event machinery + // ever confirms it. What we deliberately do NOT do is guess a "gone quiet, must be + // done" timeout: legitimate long-running-but-silent commands (a cold `tsc --noEmit` + // on a large project easily runs 20-60s with zero interim output) are + // indistinguishable from the broken-signal case by elapsed time alone, so any fixed + // threshold either fires falsely on real work or is too long to help. If neither the + // marker nor the event ever arrives, this still hangs (the pre-existing behavior) -- + // bounded only by the user's own commandExecutionTimeout / the model's agentTimeout + // at the tool layer, both already user-understood, opt-in settings. + let sawEndMarker = false + let chunkCount = 0 + let streamEndedByEvent = false + let idleTimedOut = false + const streamStartedAt = Date.now() + + // VSCode's execution.read() AsyncIterable can stay open indefinitely even after + // the command finishes — it has no built-in cancellation. We need to be able to + // break out of the loop when onDidEndTerminalShellExecution fires. We do this by + // manually driving the iterator and racing each .next() call against shellExecutionComplete. + const iterator = stream[Symbol.asyncIterator]() + let streamProcessingError: unknown = undefined - if (match !== undefined) { - data = match - this.emit("line", "") // Trigger UI to proceed + try { + // A unique sentinel to distinguish "shellExecutionComplete won the race" from a real chunk. + const DONE_SENTINEL = Symbol("done") + // A sentinel for the no-data idle timeout (see below). + const IDLE_SENTINEL = Symbol("idle") + + // How long to wait for the first chunk before assuming the command finished with + // no output (VSCode bug: { ... }-wrapped multiline commands often produce zero + // stream data AND delay onDidEndTerminalShellExecution by 60+ seconds). + // Only applies before any data arrives (chunkCount === 0). + const IDLE_TIMEOUT_MS = 3_000 + + // Hoist nextChunk outside the loop so that re-arming after an idle timeout + // reuses the same pending .next() promise instead of issuing a second call + // while the first is still unresolved (violates the async-iterator protocol + // and silently drops the first output chunk). + let nextChunk = iterator.next() + while (true) { + const racers: Promise>[] = [ + nextChunk, + shellExecutionComplete.then(() => DONE_SENTINEL as typeof DONE_SENTINEL), + ] + + // Before any data arrives, add a short idle timeout. Once data starts + // flowing we trust the stream to close normally (or the D-marker path). + if (chunkCount === 0) { + racers.push( + new Promise((resolve) => + setTimeout(() => resolve(IDLE_SENTINEL as typeof IDLE_SENTINEL), IDLE_TIMEOUT_MS), + ), + ) + } + + const raceResult = await Promise.race(racers) + + if (raceResult === DONE_SENTINEL) { + // onDidEndTerminalShellExecution fired — the shell says we're done. + // Do NOT also check doneSignal.done here: if a real chunk won the race + // but completion fired concurrently, doneSignal.done is true and the + // chunk would be dropped before being appended. Break only on the + // sentinel so data chunks always flow through even when completion races. + streamEndedByEvent = true + console.info( + `[Terminal Process] shell execution complete event broke stream loop after ${chunkCount} chunk(s), +${Date.now() - streamStartedAt}ms`, + ) + break + } + + if (raceResult === IDLE_SENTINEL) { + if (shellExecutionStarted) { + // The command is confirmed running (shell_execution_started fired). A + // silent command like `sleep 5` can legitimately produce zero output — + // elapsed time alone is not proof of completion. Re-arm the idle timer + // and keep waiting for a real chunk, the D marker, or the end event. + // Reuse the existing nextChunk promise — do NOT call iterator.next() again. + console.info( + `[Terminal Process] idle timeout fired but shell execution is running — re-arming (${chunkCount} chunks so far)`, + ) + continue + } + + const elapsedMs = Date.now() - streamStartedAt + const shellInitTimeout = Terminal.getShellIntegrationTimeout() + + if (elapsedMs < shellInitTimeout) { + // onDidStartTerminalShellExecution hasn't fired yet — the shell is + // still initializing. Don't self-finalize; re-arm the idle timer + // and keep waiting. + console.info( + `[Terminal Process] idle timeout fired but shell execution not started yet — waiting for shell init (${elapsedMs}ms elapsed)`, + ) + continue + } + + // Shell integration timeout exceeded and onDidStartTerminalShellExecution + // never fired — something went wrong during shell init. Self-finalize. + console.info( + `[Terminal Process] shell execution never started after ${elapsedMs}ms — self-finalizing`, + ) + idleTimedOut = true + console.info( + `[Terminal Process] idle timeout (${IDLE_TIMEOUT_MS}ms) after ${chunkCount} chunk(s) — self-finalizing`, + ) + break + } + + const { value: data, done } = raceResult as IteratorResult + + if (done) { + break + } + + chunkCount++ + console.info( + `[Terminal Process] stream chunk #${chunkCount} (+${Date.now() - streamStartedAt}ms, ${data.length} chars)`, + ) + + const match = this.fullOutput === "" ? this.matchAfterVsceStartMarkers(data) : undefined + + if (match !== undefined) { + this.emit("line", "") // Trigger UI to proceed + } + + // Accumulate data without filtering. + // notice to future programmers: do not add escape sequence + // filtering here: fullOutput cannot change in length (see getUnretrievedOutput), + // and chunks may not be complete so you cannot rely on detecting or removing escape sequences mid-stream. + this.fullOutput += match !== undefined ? match : data + + // For non-immediately returning commands we want to show loading spinner + // right away but this wouldn't happen until it emits a line break, so + // as soon as we get any output we emit to let webview know to show spinner + const now = Date.now() + + if (this.isListening && (now - this.lastEmitTime_ms > 100 || this.lastEmitTime_ms === 0)) { + this.emitRemainingBufferIfListening() + this.lastEmitTime_ms = now + } + + this.startHotTimer(data) + + if (this.matchBeforeVsceEndMarkers(this.fullOutput) !== undefined) { + sawEndMarker = true + console.info( + `[Terminal Process] D marker observed in stream after ${chunkCount} chunk(s), +${Date.now() - streamStartedAt}ms`, + ) + break + } + + // Advance to the next chunk only after the current one has been fully + // consumed. Calling iterator.next() here (not at the top of the loop) + // ensures there is never more than one pending .next() call at a time. + nextChunk = iterator.next() } - // Accumulate data without filtering. - // notice to future programmers: do not add escape sequence - // filtering here: fullOutput cannot change in length (see getUnretrievedOutput), - // and chunks may not be complete so you cannot rely on detecting or removing escape sequences mid-stream. - this.fullOutput += data + if (!sawEndMarker && !streamEndedByEvent) { + console.info( + `[Terminal Process] stream ended without a D marker after ${chunkCount} chunk(s), +${Date.now() - streamStartedAt}ms`, + ) + } - // For non-immediately returning commands we want to show loading spinner - // right away but this wouldn't happen until it emits a line break, so - // as soon as we get any output we emit to let webview know to show spinner - const now = Date.now() + // Set streamClosed immediately after stream ends. + this.terminal.setActiveStream(undefined) - if (this.isListening && (now - this.lastEmitTime_ms > 100 || this.lastEmitTime_ms === 0)) { - this.emitRemainingBufferIfListening() - this.lastEmitTime_ms = now + // Wait for shell execution to complete. + // + // Exit paths from the loop above: + // 1. streamEndedByEvent=true — onDidEndTerminalShellExecution already fired; + // shellExecutionComplete is resolved, nothing to await. + // 2. idleTimedOut=true — No data and no event within idle window; the command + // finished but VSCode won't tell us in time. Skip wait. + // 3. sawEndMarker=true — D marker seen in stream; event may or may not have + // fired yet. Give it a 1s grace period to arrive with + // the real exit code before proceeding without one. + // 4. Neither — stream closed naturally (no marker, no event yet); + // wait for shellExecutionComplete directly. + if (streamEndedByEvent || idleTimedOut) { + // Already resolved or deliberately skipping — nothing to wait for. + console.info( + `[Terminal Process] skipping shellExecutionComplete wait (streamEndedByEvent=${streamEndedByEvent}, idleTimedOut=${idleTimedOut})`, + ) + } else if (sawEndMarker) { + let graceTimer: NodeJS.Timeout | undefined + let graceWon = false + const grace = new Promise((resolve) => { + graceTimer = setTimeout(() => { + graceWon = true + resolve() + }, 1_000) + }) + + const waitStartedAt = Date.now() + await Promise.race([shellExecutionComplete, grace]) + clearTimeout(graceTimer) + console.info( + `[Terminal Process] post-marker wait resolved after ${Date.now() - waitStartedAt}ms via ${ + graceWon ? "grace timer (no onDidEndTerminalShellExecution)" : "shellExecutionComplete" + }`, + ) + } else { + const waitStartedAt = Date.now() + await shellExecutionComplete + console.info( + `[Terminal Process] shellExecutionComplete resolved after ${Date.now() - waitStartedAt}ms (stream closed, no D marker)`, + ) } - this.startHotTimer(data) - } + this.terminal.activeShellExecution = undefined - // Set streamClosed immediately after stream ends. - this.terminal.setActiveStream(undefined) + this.cleanupScriptFile() - // Wait for shell execution to complete. - await shellExecutionComplete - this.terminal.activeShellExecution = undefined + this.isHot = false - this.isHot = false + // Emit any remaining output before completing. + this.emitRemainingBufferIfListening() - // Emit any remaining output before completing. - this.emitRemainingBufferIfListening() + // fullOutput begins after C marker so we only need to trim off D marker + // (if D exists, see VSCode bug# 237208): + const match = this.matchBeforeVsceEndMarkers(this.fullOutput) - // fullOutput begins after C marker so we only need to trim off D marker - // (if D exists, see VSCode bug# 237208): - const match = this.matchBeforeVsceEndMarkers(this.fullOutput) + if (match !== undefined) { + this.fullOutput = match + } - if (match !== undefined) { - this.fullOutput = match - } + // For now we don't want this delaying requests since we don't send + // diagnostics automatically anymore (previous: "even though the + // command is finished, we still want to consider it 'hot' in case + // so that api request stalls to let diagnostics catch up"). + this.stopHotTimer() + this.emit("completed", this.stripCursorSequences(this.removeVSCodeShellIntegration(this.fullOutput))) + this.emit("continue") + } catch (error) { + streamProcessingError = error + console.error("[Terminal Process] Error during stream processing:", error) + } finally { + // Always release the iterator so the underlying VSCode stream can free its + // resources. (for-await-of does this automatically; our manual .next() loop + // does not, so we must call .return() explicitly on exit.) + try { + await iterator.return?.() + } catch { + /* ignore */ + } - // For now we don't want this delaying requests since we don't send - // diagnostics automatically anymore (previous: "even though the - // command is finished, we still want to consider it 'hot' in case - // so that api request stalls to let diagnostics catch up"). - this.stopHotTimer() - this.emit("completed", this.stripCursorSequences(this.removeVSCodeShellIntegration(this.fullOutput))) - this.emit("continue") + if (streamProcessingError !== undefined) { + // Ensure cleanup and caller unblocking happen even when the loop throws. + this.terminal.activeShellExecution = undefined + this.terminal.busy = false + this.isHot = false + this.cleanupScriptFile() + this.emit( + "completed", + ``, + ) + this.emit("continue") + } + } } /** - * VS Code reports each complete top-level statement in multiline input as a - * separate shell execution. Keep the submitted script in one execution so a - * leading assignment cannot complete and detach the tracked process before - * the remaining statements run. + * Prepares a multiline command for VSCode shell integration execution. + * + * For POSIX shells (bash/zsh/sh), wrapping in `{ ... }` triggers a VSCode bug + * where the shell integration stream is marked ended before read() is called, + * delivering zero chunks even though output is visible in the terminal. The + * root cause is that VSCode's multiline compound-command tracking calls + * endExecution() on the outer block before our read() arrives, so the async + * iterable returns yl.EMPTY immediately. + * + * Workaround: write the script to a temp file and run it via the shell + * executable. This produces a single-line executeCommand() call with no + * newlines, so VSCode never enters the multiline code path. + * + * PowerShell uses `. { ... }` and Fish uses `begin...end` — both have their + * own VSCode shell integration handling and are not affected by this bug. + * + * Returns the command to pass to executeCommand(), and sets this.scriptPath + * if a temp file was created (so run() can clean it up after the stream ends). */ private prepareCommandForShellIntegration( command: string, @@ -260,7 +555,71 @@ export class TerminalProcess extends BaseTerminalProcess { return `begin\n${command}\nend` } - return `{\n${command}\n}` + // POSIX shell: write to a temp script file to avoid the VSCode multiline bug. + // We need a known shell executable to run it — if we can't determine one, + // fall back to { ... } wrapping (accepts the VSCode zero-chunk bug as a + // lesser evil than invoking a non-existent "sh" on Windows). + // + // Try the Zoo Code profile first; if unset, fall back to the VS Code default + // profile so users who haven't configured a Zoo Code profile override still + // get the temp-file path instead of { ... } wrapping. + let shellExe = Terminal.getProfileShell()?.shellPath + if (!shellExe) { + const defaultProfileName = Terminal.getConfiguredDefaultProfileName() + if (defaultProfileName) { + const profiles = Terminal.getConfiguredProfiles() + const profile = profiles?.[defaultProfileName] as { path?: string | string[] } | null | undefined + if (profile) { + shellExe = Terminal.resolveProfilePath(profile.path) + } + } + } + if (!shellExe) { + return `{\n${command}\n}` + } + + // If the resolved shell is PowerShell or fish, the branches above should have + // caught it — but if shell-kind detection missed it (e.g. no explicit default + // profile configured), fall back to the appropriate wrapper rather than passing + // a .sh script to a shell that cannot execute it. + if (Terminal.isPowerShell(shellExe)) { + return `. {\n${command}\n}` + } + if (Terminal.isFish(shellExe)) { + return `begin\n${command}\nend` + } + + const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-cmd-")) + this.scriptDir = scriptDir + const scriptPath = path.join(scriptDir, "cmd.sh") + fs.writeFileSync(scriptPath, command, { mode: 0o700 }) + this.scriptPath = scriptPath + + // Quote both paths with double-quotes to handle spaces (e.g. Git Bash on + // Windows: "C:\Program Files\Git\bin\bash.exe" "C:\Users\...\roo-cmd-*.sh"). + return `"${shellExe}" "${scriptPath}"` + } + + private scriptPath: string | undefined + private scriptDir: string | undefined + + private cleanupScriptFile() { + if (this.scriptPath) { + try { + fs.unlinkSync(this.scriptPath) + } catch { + // Best-effort: if it's already gone, that's fine. + } + this.scriptPath = undefined + } + if (this.scriptDir) { + try { + fs.rmdirSync(this.scriptDir) + } catch { + // Best-effort: ignore if already removed or non-empty. + } + this.scriptDir = undefined + } } public override continue() { diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts index 7efb69b513..da4b3dd16d 100644 --- a/src/integrations/terminal/TerminalRegistry.ts +++ b/src/integrations/terminal/TerminalRegistry.ts @@ -56,14 +56,41 @@ export class TerminalRegistry { }) if (terminal instanceof Terminal) { - if (terminal.activeShellExecution === e.execution) { + // Always call read() from this event — it fires when VSCode's shell + // integration confirms the command has actually started, which is the + // earliest point at which read() will reliably capture output. Calling + // read() earlier (e.g. immediately after executeCommand()) creates a + // stream window that misses data on cold terminals where the shell + // hasn't started yet: VSCode doesn't buffer retroactively. + // + // Guard: only set the stream for the execution we own. Stale start + // events for a previous execution on the same reused terminal must + // not overwrite the current command's stream. + const process = terminal.process + const isOwnExecution = + !(process instanceof TerminalProcess) || + // Allow undefined only when the process hasn't started yet (cold + // terminal: process is assigned but run() hasn't called executeCommand). + // Once isHot is true, ownExecution is always set — a stale start + // event on a reused terminal must match exactly. + (!process.isHot && process.ownExecution === undefined) || + process.ownExecution === e.execution + if (!isOwnExecution) { + console.info( + "[TerminalRegistry] Ignoring onDidStartTerminalShellExecution for a different execution", + { terminalId: terminal.id }, + ) return } - - // Get a handle to the stream as early as possible. const stream = e.execution.read() terminal.setActiveStream(stream) - terminal.busy = true // Mark terminal as busy when shell execution starts + // Only mark busy when there is a live process to clear it later. + // If the end event already fired (early-completion race), process is + // undefined and setActiveStream returned early — setting busy here would + // leave the terminal stuck busy with nothing to clear it. + if (terminal.process) { + terminal.busy = true + } } else { console.error( "[onDidStartTerminalShellExecution] Shell execution started, but not from a Roo-registered terminal:", @@ -102,6 +129,29 @@ export class TerminalRegistry { terminal.activeShellExecution = undefined } + // Guard against a late end event for an execution that has already been + // superseded on this terminal. This can happen when a process self-finalizes + // after TerminalProcess's own D-marker grace period elapses without ever + // seeing this event (see TerminalProcess.ts's finalize()): the terminal gets + // reused for a new command before VSCode's stale event for the OLD command + // finally arrives. Without this check, that stale event would call + // shellExecutionComplete() on whatever process/exit-code tracking is + // currently attached -- the NEW command's -- corrupting its state instead of + // being a harmless no-op for the command it actually belongs to. + const isStaleExecution = + process instanceof TerminalProcess && + process.ownExecution !== undefined && + process.ownExecution !== e.execution + + if (isStaleExecution) { + console.info( + "[TerminalRegistry] Ignoring stale onDidEndTerminalShellExecution for a superseded execution", + { terminalId: terminal.id, exitCode: e.exitCode }, + ) + + return + } + if (!terminal.running) { // The end event can arrive before setActiveStream() has set // running=true (race between the global VS Code event and the diff --git a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts index 37ad5a9849..978733a593 100644 --- a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts @@ -88,6 +88,50 @@ describe("TerminalProcess", () => { } }) + it("runs command after shell integration activates via onDidChangeTerminalShellIntegration event", async () => { + // Cover Terminal.runCommand's waitForShellIntegration resolve path: shell + // integration is initially absent but arrives via the VSCode event before timeout. + let shellIntegrationHandler: ((e: any) => void) | undefined + vi.spyOn(vscode.window, "onDidChangeTerminalShellIntegration" as any).mockImplementation((handler: any) => { + shellIntegrationHandler = handler + return { dispose: vi.fn() } + }) + + mockTerminal.shellIntegration = undefined + const runPromise = mockTerminalInfo.runCommand("test command", { + onLine: vi.fn(), + onCompleted: vi.fn(), + onShellExecutionStarted: vi.fn(), + onShellExecutionComplete: vi.fn(), + }) + + // Fire the shell integration activation event — simulates VSCode telling us the + // shell is ready. waitForShellIntegration should resolve, then process.run() fires. + expect(shellIntegrationHandler).toBeDefined() + mockTerminal.shellIntegration = { + executeCommand: vi.fn().mockReturnValue({ read: vi.fn().mockReturnValue((async function* () {})()) }), + } + shellIntegrationHandler!({ terminal: mockTerminal }) + + // Give the .then() callback a chance to run process.run() + await Promise.resolve() + await Promise.resolve() + + // process.run() should have been called (shell integration is now present, so + // run() won't take the !isShellIntegrationAvailable path) + await Promise.resolve() + terminalProcess.emit( + "stream_available", + (async function* () { + yield "\x1b]633;C\x07" + yield "\x1b]633;D\x07" + })(), + ) + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + + await runPromise + }) + it("handles shell integration commands correctly", async () => { let lines: string[] = [] @@ -104,7 +148,6 @@ describe("TerminalProcess", () => { yield "More output\n" yield "Final output" yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character. - terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) })() mockExecution = { @@ -115,12 +158,160 @@ describe("TerminalProcess", () => { const runPromise = terminalProcess.run("test command") terminalProcess.emit("stream_available", mockStream) + + // onDidEndTerminalShellExecution is a separate global VSCode event, not + // something coupled to the stream iterator being pulled again -- emit it + // independently of stream consumption, matching real-world timing. + // Use setTimeout(0) so it fires after microtask-based stream processing + // (async generator iterations) has consumed all chunks including the D marker. + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + await runPromise expect(lines).toEqual(["Initial output", "More output", "Final output"]) expect(terminalProcess.isHot).toBe(false) }) + it( + "completes promptly when the D marker arrives but the stream never closes and " + + "onDidEndTerminalShellExecution never fires (VSCode #316556 / #250764)", + async () => { + let lines: string[] = [] + let completedOutput: string | undefined + + terminalProcess.on("completed", (output) => { + completedOutput = output + if (output) { + lines = output.split("\n") + } + }) + + // Simulate the confirmed real-world hang: the shell writes the D marker + // (command output is fully visible), but the stream's async iterator never + // signals `done: true` afterward, and the global onDidEndTerminalShellExecution + // event never fires. Model this with a stream that yields the D marker and + // then never resolves any further -- exactly what "never closes" looks like. + let hangForever: () => void = () => {} + mockStream = (async function* () { + yield "\x1b]633;C\x07" + yield "some output\n" + yield "\x1b]633;D\x07" + // The generator never returns past this point -- the next `.next()` call + // (which would happen if the loop kept consuming) hangs forever, and no + // `shell_execution_complete` event is ever emitted. + await new Promise((resolve) => { + hangForever = resolve + }) + })() + + mockExecution = { + read: vi.fn().mockReturnValue(mockStream), + } + + mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + + const runPromise = terminalProcess.run("test command") + terminalProcess.emit("stream_available", mockStream) + + // No shell_execution_complete is ever emitted here -- the fix must not + // depend on it to unblock once the D marker has been seen. + await runPromise + + expect(lines).toEqual(["some output", ""]) + expect(completedOutput).toBe("some output\n") + expect(terminalProcess.isHot).toBe(false) + + // Clean up the still-pending generator await so it doesn't leak between tests. + hangForever() + }, + ) + + it("does not complete a long-running, silent command until it actually produces the D marker or closes", async () => { + // A bare `sleep 60`-style command: prints the start marker and then genuinely + // nothing else for a long time because it is still running, not because VSCode + // lost the signal. There is no idle-timeout guessing -- run() simply keeps + // waiting on the stream, exactly like a real long-running command should. + let completedFired = false + + terminalProcess.on("completed", () => { + completedFired = true + }) + + // Signals once the generator has actually reached its suspension point (i.e. + // once run()'s `for await` loop has consumed the first chunk and is genuinely + // waiting on the stream for the next one), so the test can assert "not yet + // completed" and then resume deterministically, instead of guessing how many + // microtask turns run()'s internal await chain (streamAvailable, etc.) needs. + let releaseStream: () => void = () => {} + let notifySuspended: () => void = () => {} + const suspended = new Promise((resolve) => { + notifySuspended = resolve + }) + + mockStream = (async function* () { + yield "\x1b]633;C\x07" + notifySuspended() + await new Promise((resolve) => { + releaseStream = resolve + }) + yield "finally done\n" + yield "\x1b]633;D\x07" + })() + + mockExecution = { + read: vi.fn().mockReturnValue(mockStream), + } + + mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + + const runPromise = terminalProcess.run("sleep 60") + terminalProcess.emit("stream_available", mockStream) + + // Wait until the generator has genuinely suspended waiting for more input; + // it must still be waiting on the stream at this point, not completed. + await suspended + expect(completedFired).toBe(false) + + // The command finally finishes. Emit shell_execution_complete directly (as + // onDidEndTerminalShellExecution normally would) so run() doesn't need to wait + // out its 1s D-marker grace period for this assertion to be deterministic. + releaseStream() + terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) + await runPromise + + expect(completedFired).toBe(true) + }) + + it("does not drop the final chunk when shellExecutionComplete fires concurrently with the last data chunk", async () => { + // Regression for the doneSignal.done race: if shell_execution_complete fires + // while Promise.race is resolving with a real chunk, doneSignal.done flips to + // true before the continuation resumes. The loop must NOT break on doneSignal.done + // after a chunk wins — only DONE_SENTINEL triggers the early break. + let completedOutput: string | undefined + terminalProcess.once("completed", (output?: string) => { + completedOutput = output + }) + + const runPromise = terminalProcess.run("echo hello") + + // Emit the stream; the final chunk and shell_execution_complete arrive together. + terminalProcess.emit( + "stream_available", + (async function* () { + yield "\x1b]633;C\x07" + yield "hello\n" + // Emit completion concurrently with the D marker chunk so doneSignal.done + // is true when Promise.race resolves with the D marker result. + terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) + yield "\x1b]633;D\x07" + })(), + ) + + await runPromise + + expect(completedOutput).toBe("hello\n") + }) + it("wraps multiline POSIX scripts so VS Code tracks them as one shell execution", async () => { const command = 'PR_SHA=abc123\nfor f in one two; do\n echo "$f @ $PR_SHA"\ndone' @@ -221,6 +412,46 @@ describe("TerminalProcess", () => { consoleWarnSpy.mockRestore() }) + it("emits no_shell_integration with commandSubmitted=true when stream never arrives after command submission", async () => { + vi.useFakeTimers() + const prevTimeout = Terminal.getShellIntegrationTimeout() + Terminal.setShellIntegrationTimeout(50) + + try { + let commandSubmitted: boolean | undefined + let completedOutput: string | undefined + + const done = Promise.all([ + new Promise((resolve) => + terminalProcess.once("no_shell_integration", (details) => { + commandSubmitted = details.commandSubmitted + resolve() + }), + ), + new Promise((resolve) => + terminalProcess.once("completed", (output?: string) => { + completedOutput = output + resolve() + }), + ), + new Promise((resolve) => terminalProcess.once("continue", resolve)), + ]) + + // run() submits the command (shell integration IS present) but stream_available + // is never emitted, so the streamAvailable timeout fires. + const runPromise = terminalProcess.run("test command") + await vi.advanceTimersByTimeAsync(100) + await runPromise + await done + + expect(commandSubmitted).toBe(true) + expect(completedOutput).toContain("stream did not start") + } finally { + Terminal.setShellIntegrationTimeout(prevTimeout) + vi.useRealTimers() + } + }) + it("completes without warning when the execution stream is empty after submission", async () => { const noShellIntegrationSpy = vi.fn() let completedOutput: string | undefined @@ -248,10 +479,12 @@ describe("TerminalProcess", () => { terminalProcess.once("no_shell_integration", noShellIntegrationSpy) const runPromise = terminalProcess.run("test command") + // stream_available is now emitted by TerminalRegistry (onDidStartTerminalShellExecution). + // Simulate that here so run() can proceed to consume the stream. + terminalProcess.emit("stream_available", mockStream) await runPromise await eventPromises - expect(mockExecution.read).toHaveBeenCalledTimes(1) expect(completedOutput).toBe("") expect(noShellIntegrationSpy).not.toHaveBeenCalled() }) @@ -281,10 +514,12 @@ describe("TerminalProcess", () => { terminalProcess.once("no_shell_integration", noShellIntegrationSpy) const runPromise = terminalProcess.run("test command") + // stream_available is now emitted by TerminalRegistry (onDidStartTerminalShellExecution). + // Simulate that here so run() can proceed to consume the stream. + terminalProcess.emit("stream_available", mockStream) await runPromise await eventPromises - expect(mockExecution.read).toHaveBeenCalledTimes(1) expect(completedOutput).toBe("some output without marker\n") expect(noShellIntegrationSpy).not.toHaveBeenCalled() }) @@ -308,7 +543,6 @@ describe("TerminalProcess", () => { yield "still compiling...\n" yield "done" yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character. - terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) })() mockTerminal.shellIntegration.executeCommand.mockReturnValue({ @@ -319,6 +553,14 @@ describe("TerminalProcess", () => { terminalProcess.emit("stream_available", mockStream) expect(terminalProcess.isHot).toBe(true) + + // onDidEndTerminalShellExecution is a separate global VSCode event, not + // something coupled to the stream iterator being pulled again -- emit it + // independently of stream consumption, matching real-world timing. + // Use setTimeout(0) so it fires after microtask-based stream processing + // has consumed all chunks including the D marker. + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + await runPromise expect(lines).toEqual(["compiling...", "still compiling...", "done"]) @@ -326,6 +568,207 @@ describe("TerminalProcess", () => { await completePromise expect(terminalProcess.isHot).toBe(false) }) + + it("resolves waitForShellIntegration immediately when shell integration is already active", async () => { + // Cover waitForShellIntegration fast path: shellIntegration already present → resolves synchronously. + // Access the private method via bracket notation so we can call it directly. + mockTerminal.shellIntegration = { executeCommand: vi.fn() } + const result = (mockTerminalInfo as any).waitForShellIntegration(5000) + // Should resolve immediately (synchronously resolved Promise) without touching timers. + await expect(result).resolves.toBeUndefined() + }) + + it("handles executeCommand throwing by propagating the error", async () => { + mockTerminal.shellIntegration.executeCommand.mockImplementation(() => { + throw new Error("execution failed") + }) + + await expect(terminalProcess.run("bad command")).rejects.toThrow("execution failed") + }) + + it("self-finalizes via idle timeout when no stream data arrives and no event fires", async () => { + vi.useFakeTimers() + const prevTimeout = Terminal.getShellIntegrationTimeout() + Terminal.setShellIntegrationTimeout(50) + + try { + const events: string[] = [] + const done = Promise.all([ + new Promise((resolve) => + terminalProcess.once("completed", () => { + events.push("completed") + resolve() + }), + ), + new Promise((resolve) => + terminalProcess.once("continue", () => { + events.push("continue") + resolve() + }), + ), + ]) + + // An empty stream that never yields anything and never closes. + // shellExecutionComplete never fires either — simulates a completely + // silent command with no events (the idle-timeout self-finalize path). + const neverEndingStream: AsyncIterable = { + [Symbol.asyncIterator]() { + return { + next(): Promise> { + return new Promise(() => {}) // hangs forever + }, + } + }, + } + + const runPromise = terminalProcess.run("silent command") + terminalProcess.emit("stream_available", neverEndingStream) + // Let the first idle timeout (3s) fire without shellExecutionStarted. + // That keeps looping. Advance past getShellIntegrationTimeout (50ms) so + // the "shell init timeout exceeded" branch fires and we fall through to break. + await vi.advanceTimersByTimeAsync(3100) + + await runPromise + await done + + expect(events).toContain("completed") + } finally { + Terminal.setShellIntegrationTimeout(prevTimeout) + vi.useRealTimers() + } + }) + + it("passes a temp-script invocation to executeCommand for multiline POSIX commands when profile shell is set", async () => { + vi.spyOn(Terminal, "getProfileShell").mockReturnValue({ + shellPath: "/bin/bash", + shellArgs: undefined, + }) + + const command = "echo a\necho b" + const runPromise = terminalProcess.run(command) + terminalProcess.emit( + "stream_available", + (async function* () { + yield "\x1b]633;C\x07" + yield "a\n" + yield "b\n" + yield "\x1b]633;D\x07" + })(), + ) + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + + await runPromise + + // executeCommand should be called with a shell + temp-script invocation, not the + // raw multiline command string. + const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + expect(calledWith).toMatch(/^"\/bin\/bash" ".*roo-cmd-.*\.sh"$/) + }) + + it("uses VS Code default profile shell for temp-script when no Zoo Code profile override is set", async () => { + vi.spyOn(Terminal, "getProfileShell").mockReturnValue(undefined) + vi.spyOn(Terminal, "getConfiguredDefaultProfileName").mockReturnValue("bash") + vi.spyOn(Terminal, "getConfiguredProfiles").mockReturnValue({ + bash: { path: "/bin/bash" }, + }) + // resolveProfilePath calls existsSync, which returns false on Windows for POSIX + // paths. Mock it so the test is platform-independent. + vi.spyOn(Terminal, "resolveProfilePath").mockReturnValue("/bin/bash") + + const command = "echo a\necho b" + const runPromise = terminalProcess.run(command) + terminalProcess.emit( + "stream_available", + (async function* () { + yield "\x1b]633;C\x07" + yield "a\n" + yield "b\n" + yield "\x1b]633;D\x07" + })(), + ) + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + + await runPromise + + const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + expect(calledWith).toMatch(/^"\/bin\/bash" ".*roo-cmd-.*\.sh"$/) + }) + + it("uses PS dot-source wrapping when the default profile resolves to PowerShell (not a .sh temp-script)", async () => { + vi.spyOn(Terminal, "getProfileShell").mockReturnValue(undefined) + vi.spyOn(Terminal, "isActiveShellPowerShell").mockReturnValue(false) // detection missed it + vi.spyOn(Terminal, "isActiveShellFish").mockReturnValue(false) + vi.spyOn(Terminal, "getConfiguredDefaultProfileName").mockReturnValue("Windows PowerShell") + vi.spyOn(Terminal, "getConfiguredProfiles").mockReturnValue({ + "Windows PowerShell": { path: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" }, + }) + vi.spyOn(Terminal, "resolveProfilePath").mockReturnValue( + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + ) + + const command = "echo a\necho b" + const runPromise = terminalProcess.run(command) + terminalProcess.emit( + "stream_available", + (async function* () { + yield "\x1b]633;C\x07" + yield "a\n" + yield "b\n" + yield "\x1b]633;D\x07" + })(), + ) + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + + await runPromise + + const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + expect(calledWith).toBe(`. {\necho a\necho b\n}`) + }) + + it("completes cleanly when shellExecutionComplete fires before stream_available", async () => { + const completedOutputs: string[] = [] + terminalProcess.on("completed", (output) => completedOutputs.push(output ?? "")) + + // Emit shell_execution_complete on the next tick — after run() has registered its + // once("shell_execution_complete") listener but before stream_available fires. + // This simulates a zero-output command where the end event beats the stream event. + setTimeout(() => terminalProcess.emit("shell_execution_complete", { exitCode: 0 }), 0) + + await terminalProcess.run("echo hello") + + expect(completedOutputs).toEqual([""]) + expect(terminalProcess.isHot).toBe(false) + expect(mockTerminalInfo.busy).toBe(false) + expect(mockTerminalInfo.activeShellExecution).toBeUndefined() + }) + + it("does not leave terminal busy when onDidStartTerminalShellExecution fires after early completion", async () => { + // Simulate the production race: end event arrives before the stream. + // The registry's onDidEndTerminalShellExecution handler (running=false branch) calls + // terminal.shellExecutionComplete(), which clears terminal.process = undefined. + // A late onDidStartTerminalShellExecution then arrives: setActiveStream() returns + // early (no process), and TerminalRegistry must not set busy = true afterward. + + // Step 1: end fires before run() sets running=true (the !terminal.running registry branch). + // Drive this by having the shell_execution_complete event clear terminal.process + // the same way shellExecutionComplete() does. + setTimeout(() => mockTerminalInfo.shellExecutionComplete({ exitCode: 0, signal: undefined }), 0) + await terminalProcess.run("echo hello") + + // terminal.process was cleared by shellExecutionComplete(). + expect(mockTerminalInfo.process).toBeUndefined() + expect(mockTerminalInfo.busy).toBe(false) + + // Step 2: late start event arrives — setActiveStream returns early (no process). + // Replicate the TerminalRegistry guard: only set busy when process exists. + const lateStream = (async function* () {})() + mockTerminalInfo.setActiveStream(lateStream) + if (mockTerminalInfo.process) { + mockTerminalInfo.busy = true + } + + expect(mockTerminalInfo.busy).toBe(false) + }) }) describe("continue", () => { @@ -466,6 +909,15 @@ describe("TerminalProcess", () => { expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length - "previous".length) }) + + it("trims at OSC 133;D when only a 133 end marker is present (no 633 marker)", () => { + // Line 598: endIndex = index133 branch — only the 133 end marker exists. + terminalProcess["fullOutput"] = "hello\x1b]133;D\x07world" + terminalProcess["lastRetrievedIndex"] = 0 + + const unretrieved = terminalProcess.getUnretrievedOutput() + expect(unretrieved).toBe("hello") + }) }) describe("interpretExitCode", () => { diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts index c70bcc15f2..8e9af919ea 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts @@ -176,11 +176,17 @@ async function testTerminalCommand( // Set up the mock stream with real command output and exit code const { stream, exitCode } = createRealCommandStream(command) - // Configure the mock terminal to return our stream + // Configure the mock terminal to return our stream. Reused as the SAME object + // for the start/end event triggers below -- TerminalRegistry's end handler + // correlates events to TerminalProcess.ownExecution by identity (see #800 fix), + // so a real VSCode-like flow must reference this same execution throughout, + // exactly as the real API's TerminalShellExecution object would be. + const mockExecution = { + commandLine: { value: command }, + read: vi.fn().mockReturnValue(stream), + } mockTerminal.shellIntegration.executeCommand.mockImplementation(function () { - return { - read: vi.fn().mockReturnValue(stream), - } + return mockExecution }) // Set up event listeners to capture output @@ -218,24 +224,25 @@ async function testTerminalCommand( if (eventHandlers.startTerminalShellExecution) { eventHandlers.startTerminalShellExecution({ terminal: mockTerminal, - execution: { - commandLine: { value: command }, - read: () => stream, - }, + execution: mockExecution, }) } - // Wait for some output to be processed - await new Promise((resolve) => { - terminalProcess.once("line", () => resolve()) - }) - - // Then trigger the end event + // Trigger the end event after microtask-based stream consumption completes. + // The stream yields chunks as microtasks (async generator); setTimeout(0) + // fires after all pending microtasks, so the D marker will be consumed and + // the loop will have broken on sawEndMarker before this fires. Firing before + // that (e.g. after the first 'line' event) would cause DONE_SENTINEL to win + // the Promise.race and skip unconsumed chunks. if (eventHandlers.endTerminalShellExecution) { - eventHandlers.endTerminalShellExecution({ - terminal: mockTerminal, - exitCode: exitCode, - }) + const _exitCode = exitCode + setTimeout(() => { + eventHandlers.endTerminalShellExecution({ + terminal: mockTerminal, + execution: mockExecution, + exitCode: _exitCode, + }) + }, 0) } // Store exit details for return diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts index 3464fbd9a2..e129160731 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts @@ -118,11 +118,17 @@ async function testCmdCommand( ;({ stream, exitCode } = createCmdCommandStream(command)) } - // Configure the mock terminal to return our stream + // Configure the mock terminal to return our stream. Reused as the SAME object + // for the start/end event triggers below -- TerminalRegistry's end handler + // correlates events to TerminalProcess.ownExecution by identity (see #800 fix), + // so a real VSCode-like flow must reference this same execution throughout, + // exactly as the real API's TerminalShellExecution object would be. + const mockExecution = { + commandLine: { value: command }, + read: vi.fn().mockReturnValue(stream), + } mockTerminal.shellIntegration.executeCommand.mockImplementation(function () { - return { - read: vi.fn().mockReturnValue(stream), - } + return mockExecution }) // Set up event listeners to capture output @@ -157,10 +163,7 @@ async function testCmdCommand( if (eventHandlers.startTerminalShellExecution) { eventHandlers.startTerminalShellExecution({ terminal: mockTerminal, - execution: { - commandLine: { value: command }, - read: () => stream, - }, + execution: mockExecution, }) } @@ -182,12 +185,20 @@ async function testCmdCommand( }, 500) }) - // Then trigger the end event + // Trigger the end event after microtask-based stream consumption completes. + // The stream yields chunks as microtasks (async generator); setTimeout(0) + // fires after all pending microtasks, so all chunks (including the D marker) + // will be consumed before this fires. Firing synchronously would let + // DONE_SENTINEL win the Promise.race and skip unconsumed chunks. if (eventHandlers.endTerminalShellExecution) { - eventHandlers.endTerminalShellExecution({ - terminal: mockTerminal, - exitCode: exitCode, - }) + const _exitCode = exitCode + setTimeout(() => { + eventHandlers.endTerminalShellExecution({ + terminal: mockTerminal, + execution: mockExecution, + exitCode: _exitCode, + }) + }, 0) } // Store exit details for return diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts index a765fca85b..6f82634110 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts @@ -119,11 +119,17 @@ async function testPowerShellCommand( ;({ stream, exitCode } = createPowerShellStream(command)) } - // Configure the mock terminal to return our stream + // Configure the mock terminal to return our stream. Reused as the SAME object + // for the start/end event triggers below -- TerminalRegistry's end handler + // correlates events to TerminalProcess.ownExecution by identity (see #800 fix), + // so a real VSCode-like flow must reference this same execution throughout, + // exactly as the real API's TerminalShellExecution object would be. + const mockExecution = { + commandLine: { value: command }, + read: vi.fn().mockReturnValue(stream), + } mockTerminal.shellIntegration.executeCommand.mockImplementation(function () { - return { - read: vi.fn().mockReturnValue(stream), - } + return mockExecution }) // Set up event listeners to capture output @@ -158,10 +164,7 @@ async function testPowerShellCommand( if (eventHandlers.startTerminalShellExecution) { eventHandlers.startTerminalShellExecution({ terminal: mockTerminal, - execution: { - commandLine: { value: command }, - read: () => stream, - }, + execution: mockExecution, }) } @@ -183,10 +186,11 @@ async function testPowerShellCommand( }, 500) }) - // Then trigger the end event + // Then trigger the end event, referencing the SAME execution object as above. if (eventHandlers.endTerminalShellExecution) { eventHandlers.endTerminalShellExecution({ terminal: mockTerminal, + execution: mockExecution, exitCode: exitCode, }) } diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts index 4310a9d9e0..00cfaa3bc9 100644 --- a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -367,6 +367,73 @@ describe("Terminal VS Code terminal profile (#277)", () => { expect(Terminal.isActiveShellPowerShell("win32")).toBe(true) }) }) + + describe("isActiveShellFish", () => { + it("returns false when a custom profile resolves to a non-fish shell", () => { + stubProfiles({ linux: { "My Bash": { path: "/bin/bash" } } }) + Terminal.setTerminalProfile("My Bash") + expect(Terminal.isActiveShellFish("linux")).toBe(false) + }) + + it("returns true when a custom profile resolves to fish", () => { + stubProfiles({ linux: { "Fish Shell": { path: "/usr/bin/fish" } } }) + Terminal.setTerminalProfile("Fish Shell") + expect(Terminal.isActiveShellFish("linux")).toBe(true) + }) + + it("returns false when no override and no default profile name is configured", () => { + Terminal.setTerminalProfile(undefined) + getConfigurationSpy = vi.spyOn(vscode.workspace, "getConfiguration").mockReturnValue({ + get: (_key: string, defaultValue?: unknown) => defaultValue, + inspect: () => undefined, + } as any) + expect(Terminal.isActiveShellFish("linux")).toBe(false) + }) + + it("returns false when the default profile entry is null", () => { + Terminal.setTerminalProfile(undefined) + getConfigurationSpy = vi + .spyOn(vscode.workspace, "getConfiguration") + .mockImplementation((section?: string) => { + if (section === "terminal.integrated.profiles") { + return { + inspect: (_key: string) => ({ defaultValue: { "Fish Shell": null } }), + } as any + } + if (section === "terminal.integrated") { + return { + inspect: (key: string) => + key === "defaultProfile.linux" ? { defaultValue: "Fish Shell" } : undefined, + } as any + } + return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + }) + expect(Terminal.isActiveShellFish("linux")).toBe(false) + }) + + it("returns true when the default profile resolves to fish", () => { + Terminal.setTerminalProfile(undefined) + getConfigurationSpy = vi + .spyOn(vscode.workspace, "getConfiguration") + .mockImplementation((section?: string) => { + if (section === "terminal.integrated.profiles") { + return { + inspect: (_key: string) => ({ + defaultValue: { "Fish Shell": { path: "/usr/local/bin/fish" } }, + }), + } as any + } + if (section === "terminal.integrated") { + return { + inspect: (key: string) => + key === "defaultProfile.linux" ? { defaultValue: "Fish Shell" } : undefined, + } as any + } + return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + }) + expect(Terminal.isActiveShellFish("linux")).toBe(true) + }) + }) }) describe("getProfileShell", () => { diff --git a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts index c388c946ca..dbfb362d52 100644 --- a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode" import { ExecaTerminal } from "../ExecaTerminal" import { ShellIntegrationManager } from "../ShellIntegrationManager" import { Terminal } from "../Terminal" +import { TerminalProcess } from "../TerminalProcess" import { TerminalRegistry } from "../TerminalRegistry" const PAGER = process.platform === "win32" ? "" : "cat" @@ -206,6 +207,7 @@ describe("TerminalRegistry", () => { }) describe("onDidEndTerminalShellExecution race condition (#489, #622)", () => { + let startHandler: (e: any) => Promise let endHandler: (e: any) => Promise beforeEach(() => { @@ -217,9 +219,10 @@ describe("TerminalRegistry", () => { ;(vscode.window as any).onDidStartTerminalShellExecution ??= () => ({ dispose: () => {} }) ;(vscode.window as any).onDidEndTerminalShellExecution ??= () => ({ dispose: () => {} }) - vi.spyOn(vscode.window, "onDidStartTerminalShellExecution" as any).mockImplementation((_handler: any) => ({ - dispose: vi.fn(), - })) + vi.spyOn(vscode.window, "onDidStartTerminalShellExecution" as any).mockImplementation((handler: any) => { + startHandler = handler + return { dispose: vi.fn() } + }) vi.spyOn(vscode.window, "onDidEndTerminalShellExecution" as any).mockImplementation((handler: any) => { endHandler = handler @@ -285,6 +288,103 @@ describe("TerminalRegistry", () => { expect(terminal.busy).toBe(false) expect(completeSpy).not.toHaveBeenCalled() }) + + it( + "ignores a late end event for a superseded execution instead of completing " + + "the next command on the same reused terminal", + async () => { + const terminal = TerminalRegistry.createTerminal("/test/path", "vscode") as Terminal + + // Command A: started, then self-finalized (e.g. TerminalProcess's own D-marker + // grace period elapsed without ever seeing onDidEndTerminalShellExecution -- + // see TerminalProcess.ts's finalize()). Its own execution reference is what the + // registry must compare against, since terminal.activeShellExecution may + // already be pointing at a different (or no) execution by the time A's stale + // event finally arrives. + const processA = new TerminalProcess(terminal) + const executionA = { commandLine: { value: "command A" } } as any + processA.ownExecution = executionA + const emitA = vi.spyOn(processA, "emit") + + // Command B has since started on the SAME reused terminal -- this is the + // state TerminalRegistry sees by the time A's late event arrives: a live + // process with its own, different execution. + const processB = new TerminalProcess(terminal) + const executionB = { commandLine: { value: "command B" } } as any + processB.ownExecution = executionB + terminal.process = processB + terminal.running = true + const emitB = vi.spyOn(processB, "emit") + + // A's end event finally arrives, referencing A's (now superseded) execution. + await endHandler({ + terminal: terminal.terminal, + execution: executionA, + exitCode: 1, + }) + + // B must be completely unaffected: no shell_execution_complete delivered to + // either process, and B is still the terminal's live process. + expect(emitA).not.toHaveBeenCalled() + expect(emitB).not.toHaveBeenCalled() + expect(terminal.process).toBe(processB) + expect(terminal.running).toBe(true) + + // B's own end event, referencing B's execution, must still work normally. + await endHandler({ + terminal: terminal.terminal, + execution: executionB, + exitCode: 0, + }) + + expect(emitB).toHaveBeenCalledWith("shell_execution_complete", expect.objectContaining({ exitCode: 0 })) + }, + ) + + it("ignores a start event for a different (stale) execution and does not overwrite the stream", async () => { + const terminal = TerminalRegistry.createTerminal("/test/path", "vscode") as Terminal + + // processA owns executionA — this is the current, live command. + const processA = new TerminalProcess(terminal) + const executionA = { commandLine: { value: "command A" }, read: vi.fn() } as any + processA.ownExecution = executionA + terminal.process = processA + const setStreamSpy = vi.spyOn(terminal, "setActiveStream") + + // A stale start event arrives referencing a *different* execution (executionB). + // This can happen when VSCode fires a delayed start event for a prior terminal + // session on the same reused terminal object. + const executionB = { commandLine: { value: "stale command" }, read: vi.fn() } as any + await startHandler({ + terminal: terminal.terminal, + execution: executionB, + }) + + // The stale event must be ignored: read() must not be called on it and the + // terminal's active stream must not be replaced. + expect(executionB.read).not.toHaveBeenCalled() + expect(setStreamSpy).not.toHaveBeenCalled() + }) + + it("sets the stream when the start event matches the process's own execution", async () => { + const terminal = TerminalRegistry.createTerminal("/test/path", "vscode") as Terminal + + const process = new TerminalProcess(terminal) + const mockStream = (async function* () {})() + const execution = { commandLine: { value: "echo hi" }, read: vi.fn().mockReturnValue(mockStream) } as any + process.ownExecution = execution + terminal.process = process + const setStreamSpy = vi.spyOn(terminal, "setActiveStream") + + await startHandler({ + terminal: terminal.terminal, + execution, + }) + + expect(execution.read).toHaveBeenCalledTimes(1) + expect(setStreamSpy).toHaveBeenCalledWith(mockStream) + expect(terminal.busy).toBe(true) + }) }) describe("releaseTerminalsForTask", () => { From e482d1146ea09bb65b57595f030a494c17c5c087 Mon Sep 17 00:00:00 2001 From: Doedja <111399314+doedja@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:36:47 +0700 Subject: [PATCH 11/89] feat(kenari): add Kenari as a first-class provider (#793) * feat(kenari): add Kenari as a first-class provider * test(kenari): cover provider registration arms and stream branches * test(kenari): flip remaining partial branches, drop dead reasoning fallback * refactor(kenari): address review feedback - Reuse the shared extractReasoningFromDelta helper in the stream handler instead of a hand-rolled reasoning_content cast, matching opencode-go; this also adds the OpenRouter-style `reasoning` fallback. New handler test covers the fallback path. - Fetcher spec: assert the surviving entry's contextWindow in the skip-invalid test so a field-mapping regression in that branch is caught, and add a parseKenariModel test that locks the context_window/max_output_tokens precedence over their aliases. - Hoist KENARI_BASE_URL into @roo-code/types so the handler baseURL and the fetcher endpoint share one source of truth. --- packages/types/src/global-settings.ts | 1 + packages/types/src/provider-settings.ts | 11 + packages/types/src/providers/index.ts | 4 + packages/types/src/providers/kenari.ts | 26 ++ src/api/__tests__/index.spec.ts | 29 ++ src/api/index.ts | 3 + src/api/providers/__tests__/kenari.spec.ts | 395 ++++++++++++++++++ .../fetchers/__tests__/kenari.spec.ts | 146 +++++++ .../fetchers/__tests__/modelCache.spec.ts | 20 + src/api/providers/fetchers/kenari.ts | 98 +++++ src/api/providers/fetchers/modelCache.ts | 8 +- src/api/providers/index.ts | 1 + src/api/providers/kenari.ts | 151 +++++++ .../webview/__tests__/ClineProvider.spec.ts | 6 + .../__tests__/webviewMessageHandler.spec.ts | 39 ++ src/core/webview/webviewMessageHandler.ts | 18 + src/shared/api.ts | 1 + .../src/components/settings/ApiOptions.tsx | 12 + .../src/components/settings/ModelPicker.tsx | 1 + .../settings/__tests__/ApiOptions.spec.tsx | 39 ++ .../src/components/settings/constants.ts | 1 + .../components/settings/providers/Kenari.tsx | 82 ++++ .../providers/__tests__/Kenari.spec.tsx | 90 ++++ .../components/settings/providers/index.ts | 1 + .../settings/utils/providerModelConfig.ts | 2 + .../hooks/__tests__/useSelectedModel.spec.ts | 74 ++++ .../components/ui/hooks/useSelectedModel.ts | 8 + webview-ui/src/i18n/locales/ca/settings.json | 2 + webview-ui/src/i18n/locales/de/settings.json | 2 + webview-ui/src/i18n/locales/en/settings.json | 2 + webview-ui/src/i18n/locales/es/settings.json | 2 + webview-ui/src/i18n/locales/fr/settings.json | 2 + webview-ui/src/i18n/locales/hi/settings.json | 2 + webview-ui/src/i18n/locales/id/settings.json | 2 + webview-ui/src/i18n/locales/it/settings.json | 2 + webview-ui/src/i18n/locales/ja/settings.json | 2 + webview-ui/src/i18n/locales/ko/settings.json | 2 + webview-ui/src/i18n/locales/nl/settings.json | 2 + webview-ui/src/i18n/locales/pl/settings.json | 2 + .../src/i18n/locales/pt-BR/settings.json | 2 + webview-ui/src/i18n/locales/ru/settings.json | 2 + webview-ui/src/i18n/locales/tr/settings.json | 2 + webview-ui/src/i18n/locales/vi/settings.json | 2 + .../src/i18n/locales/zh-CN/settings.json | 2 + .../src/i18n/locales/zh-TW/settings.json | 2 + .../src/utils/__tests__/validate.spec.ts | 35 ++ webview-ui/src/utils/validate.ts | 5 + 47 files changed, 1340 insertions(+), 3 deletions(-) create mode 100644 packages/types/src/providers/kenari.ts create mode 100644 src/api/__tests__/index.spec.ts create mode 100644 src/api/providers/__tests__/kenari.spec.ts create mode 100644 src/api/providers/fetchers/__tests__/kenari.spec.ts create mode 100644 src/api/providers/fetchers/kenari.ts create mode 100644 src/api/providers/kenari.ts create mode 100644 webview-ui/src/components/settings/providers/Kenari.tsx create mode 100644 webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 802210626c..76420bc020 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -319,6 +319,7 @@ export const SECRET_STATE_KEYS = [ "friendliApiKey", "vercelAiGatewayApiKey", "opencodeGoApiKey", + "kenariApiKey", "basetenApiKey", ] as const diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index d91d042a84..fb03531c33 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -47,6 +47,7 @@ export const dynamicProviders = [ "poe", "deepseek", "opencode-go", + "kenari", ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -413,6 +414,11 @@ const opencodeGoSchema = baseProviderSettingsSchema.extend({ opencodeGoModelId: z.string().optional(), }) +const kenariSchema = baseProviderSettingsSchema.extend({ + kenariApiKey: z.string().optional(), + kenariModelId: z.string().optional(), +}) + const zooGatewaySchema = baseProviderSettingsSchema.extend({ zooSessionToken: z.string().optional(), zooGatewayModelId: z.string().optional(), @@ -459,6 +465,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), opencodeGoSchema.merge(z.object({ apiProvider: z.literal("opencode-go") })), + kenariSchema.merge(z.object({ apiProvider: z.literal("kenari") })), zooGatewaySchema.merge(z.object({ apiProvider: z.literal("zoo-gateway") })), defaultSchema, ]) @@ -496,6 +503,7 @@ export const providerSettingsSchema = z.object({ ...qwenCodeSchema.shape, ...vercelAiGatewaySchema.shape, ...opencodeGoSchema.shape, + ...kenariSchema.shape, ...zooGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -528,6 +536,7 @@ export const modelIdKeys = [ "litellmModelId", "vercelAiGatewayModelId", "opencodeGoModelId", + "kenariModelId", "zooGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] @@ -576,6 +585,7 @@ export const modelIdKeysByProvider: Record = { friendli: "apiModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", "opencode-go": "opencodeGoModelId", + kenari: "kenariModelId", "zoo-gateway": "zooGatewayModelId", } @@ -715,6 +725,7 @@ export const MODELS_BY_PROVIDER: Record< unbound: { id: "unbound", label: "Unbound", models: [] }, "vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] }, "opencode-go": { id: "opencode-go", label: "Opencode Go", models: [] }, + kenari: { id: "kenari", label: "Kenari", models: [] }, "zoo-gateway": { id: "zoo-gateway", label: "Zoo Gateway", models: [] }, // Local providers; models discovered from localhost endpoints. diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index bcb4a76d5d..77b0415898 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -24,6 +24,7 @@ export * from "./vscode-llm.js" export * from "./xai.js" export * from "./vercel-ai-gateway.js" export * from "./opencode-go.js" +export * from "./kenari.js" export * from "./zai.js" export * from "./minimax.js" export * from "./mimo.js" @@ -51,6 +52,7 @@ import { vscodeLlmDefaultModelId } from "./vscode-llm.js" import { xaiDefaultModelId } from "./xai.js" import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { opencodeGoDefaultModelId } from "./opencode-go.js" +import { kenariDefaultModelId } from "./kenari.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" import { minimaxDefaultModelId } from "./minimax.js" import { mimoDefaultModelId } from "./mimo.js" @@ -125,6 +127,8 @@ export function getProviderDefaultModelId( return vercelAiGatewayDefaultModelId case "opencode-go": return opencodeGoDefaultModelId + case "kenari": + return kenariDefaultModelId case "zoo-gateway": return zooGatewayDefaultModelId case "anthropic": diff --git a/packages/types/src/providers/kenari.ts b/packages/types/src/providers/kenari.ts new file mode 100644 index 0000000000..3b14b5f746 --- /dev/null +++ b/packages/types/src/providers/kenari.ts @@ -0,0 +1,26 @@ +import type { ModelInfo } from "../model.js" + +// Kenari: Indonesian OpenAI-compatible AI gateway billed in Rupiah (IDR). +// https://kenari.id/docs · base URL: https://kenari.id/v1 +// +// The full model list (and metadata) is fetched dynamically from +// `https://kenari.id/v1/models`, so models can be switched on the fly. +// The values below are only a fallback used before the live list resolves. + +// Single source of truth for the gateway base URL, shared by the handler and +// the model fetcher so the endpoint is defined in exactly one place. +export const KENARI_BASE_URL = "https://kenari.id/v1" + +export const kenariDefaultModelId = "glm-5-2" + +export const kenariDefaultModelInfo: ModelInfo = { + maxTokens: 32_768, + contextWindow: 1_048_576, + supportsImages: false, + supportsPromptCache: false, + // Pricing is intentionally omitted: Kenari bills in IDR (micro-rupiah per 1M tokens), + // which cannot be rendered in the USD price fields without a misleading conversion. + description: "Kenari model. Available models and metadata are resolved dynamically from /v1/models.", +} + +export const KENARI_DEFAULT_TEMPERATURE = 0 diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts new file mode 100644 index 0000000000..b86e73dac6 --- /dev/null +++ b/src/api/__tests__/index.spec.ts @@ -0,0 +1,29 @@ +// npx vitest run src/api/__tests__/index.spec.ts + +// Mock vscode first to avoid import errors +vitest.mock("vscode", () => ({ + workspace: { + getConfiguration: () => ({ + get: (_key: string, defaultValue?: unknown) => defaultValue, + }), + }, +})) + +import type { ProviderSettings } from "@roo-code/types" + +import { buildApiHandler } from "../index" +import { KenariHandler } from "../providers/kenari" + +describe("buildApiHandler", () => { + it("returns a KenariHandler for the kenari provider", () => { + const configuration: ProviderSettings = { + apiProvider: "kenari", + kenariApiKey: "test-key", + kenariModelId: "glm-5-2", + } + + const handler = buildApiHandler(configuration) + + expect(handler).toBeInstanceOf(KenariHandler) + }) +}) diff --git a/src/api/index.ts b/src/api/index.ts index 6faa524e99..5a9532b43d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -34,6 +34,7 @@ import { FriendliHandler, VercelAiGatewayHandler, OpencodeGoHandler, + KenariHandler, ZooGatewayHandler, MiniMaxHandler, MimoHandler, @@ -196,6 +197,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new VercelAiGatewayHandler(options) case "opencode-go": return new OpencodeGoHandler(options) + case "kenari": + return new KenariHandler(options) case "zoo-gateway": return new ZooGatewayHandler(options) case "minimax": diff --git a/src/api/providers/__tests__/kenari.spec.ts b/src/api/providers/__tests__/kenari.spec.ts new file mode 100644 index 0000000000..28691437a2 --- /dev/null +++ b/src/api/providers/__tests__/kenari.spec.ts @@ -0,0 +1,395 @@ +// npx vitest run src/api/providers/__tests__/kenari.spec.ts + +// Mock vscode first to avoid import errors +vitest.mock("vscode", () => ({ + workspace: { + getConfiguration: () => ({ + get: (_key: string, defaultValue?: unknown) => defaultValue, + }), + }, +})) + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { kenariDefaultModelId } from "@roo-code/types" + +import { KenariHandler } from "../kenari" +import { getModels } from "../fetchers/modelCache" +import { ApiHandlerOptions } from "../../../shared/api" + +vitest.mock("openai") +vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) })) +vitest.mock("../fetchers/modelCache", () => ({ + getModels: vitest.fn().mockImplementation(() => + Promise.resolve({ + "glm-5-2": { + maxTokens: 32768, + contextWindow: 1048576, + supportsImages: false, + supportsPromptCache: false, + description: "GLM 5.2", + }, + }), + ), + getModelsFromCache: vitest.fn().mockReturnValue(undefined), +})) + +const mockCreate = vitest.fn() + +;(OpenAI as any).mockImplementation(function () { + return { + chat: { completions: { create: mockCreate } }, + } +}) + +describe("KenariHandler", () => { + const mockOptions: ApiHandlerOptions = { + kenariApiKey: "test-key", + kenariModelId: "glm-5-2", + } + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate.mockClear() + }) + + it("initializes the OpenAI client with the Kenari base URL and key", () => { + const handler = new KenariHandler(mockOptions) + expect(handler).toBeInstanceOf(KenariHandler) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://kenari.id/v1", + apiKey: "test-key", + }), + ) + }) + + describe("fetchModel", () => { + it("returns the configured model info", async () => { + const handler = new KenariHandler(mockOptions) + const result = await handler.fetchModel() + expect(result.id).toBe("glm-5-2") + expect(result.info.maxTokens).toBe(32768) + expect(result.info.contextWindow).toBe(1048576) + expect(result.info.supportsPromptCache).toBe(false) + }) + + it("falls back to the default model id when none is configured", async () => { + const handler = new KenariHandler({ kenariApiKey: "test-key" }) + const result = await handler.fetchModel() + expect(result.id).toBe(kenariDefaultModelId) + }) + }) + + describe("createMessage", () => { + beforeEach(() => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + content: "Hello", + reasoning_content: "thinking…", + tool_calls: [ + { + index: 0, + id: "call_1", + function: { name: "read_file", arguments: '{"path":' }, + }, + ], + }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { + prompt_tokens: 12, + completion_tokens: 7, + total_tokens: 19, + prompt_tokens_details: { cached_tokens: 4 }, + }, + } + }, + })) + }) + + it("streams text, reasoning, tool-call and usage chunks", async () => { + const handler = new KenariHandler(mockOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const chunks = [] + for await (const chunk of handler.createMessage("You are helpful.", messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "text", text: "Hello" }) + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" }) + expect(chunks).toContainEqual({ + type: "tool_call_partial", + index: 0, + id: "call_1", + name: "read_file", + arguments: '{"path":', + }) + expect(chunks).toContainEqual({ + type: "usage", + inputTokens: 12, + outputTokens: 7, + cacheReadTokens: 4, + }) + }) + + it("yields nothing for a chunk whose delta has no content, reasoning or tool calls", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { choices: [{ delta: {}, index: 0 }], usage: null } + yield { choices: [], usage: null } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toEqual([]) + }) + + it("streams tool call chunks even when the function name and arguments are missing", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { tool_calls: [{ index: 1 }] }, index: 0 }], + usage: null, + } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { + type: "tool_call_partial", + index: 1, + id: undefined, + name: undefined, + arguments: undefined, + }, + ]) + }) + + it("reports undefined cache reads when usage has no prompt_tokens_details", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: {}, index: 0 }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { + type: "usage", + inputTokens: 3, + outputTokens: 2, + cacheReadTokens: undefined, + }, + ]) + }) + + it("skips the reasoning chunk when reasoning_content is an empty string", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Hi", reasoning_content: "" }, index: 0 }], + usage: null, + } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toEqual([{ type: "text", text: "Hi" }]) + }) + + it("emits reasoning from the OpenRouter-style `reasoning` field when reasoning_content is absent", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Hi", reasoning: "thinking…" }, index: 0 }], + usage: null, + } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" }) + }) + + it("omits temperature for models that do not support it", async () => { + vitest.mocked(getModels).mockResolvedValueOnce({ + "openai/o3-mini": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + description: "o3-mini via Kenari", + }, + }) + + const handler = new KenariHandler({ kenariApiKey: "test-key", kenariModelId: "openai/o3-mini" }) + for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + void _chunk // drain + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "openai/o3-mini", + temperature: undefined, + }), + ) + }) + + it("sends an explicitly configured model temperature", async () => { + const handler = new KenariHandler({ ...mockOptions, modelTemperature: 0.7 }) + for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + void _chunk // drain + } + + expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0.7 })) + }) + + it("honors metadata.parallelToolCalls false", async () => { + const handler = new KenariHandler(mockOptions) + for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }], { + taskId: "task-1", + parallelToolCalls: false, + })) { + void _chunk // drain + } + + expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ parallel_tool_calls: false })) + }) + + it("reports zero usage when the upstream counts are zero", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "x" }, index: 0 }], + usage: { prompt_tokens: 0, completion_tokens: 0 }, + } + }, + })) + + const handler = new KenariHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ + type: "usage", + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: undefined, + }) + }) + + it("requests a streaming completion with usage included", async () => { + const handler = new KenariHandler(mockOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + for await (const _chunk of handler.createMessage("sys", messages)) { + void _chunk // drain + } + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5-2", + stream: true, + stream_options: { include_usage: true }, + max_completion_tokens: 32768, + temperature: expect.any(Number), + }), + ) + }) + }) + + describe("completePrompt", () => { + it("returns the message content for a non-streaming completion", async () => { + mockCreate.mockResolvedValue({ choices: [{ message: { content: "the answer" } }] }) + const handler = new KenariHandler(mockOptions) + expect(await handler.completePrompt("ping")).toBe("the answer") + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5-2", + stream: false, + max_completion_tokens: 32768, + }), + ) + }) + + it("returns an empty string when the completion has no content", async () => { + mockCreate.mockResolvedValue({ choices: [{ message: { content: null } }] }) + const handler = new KenariHandler(mockOptions) + expect(await handler.completePrompt("ping")).toBe("") + }) + + it("wraps errors with a Kenari-specific message", async () => { + mockCreate.mockRejectedValue(new Error("boom")) + const handler = new KenariHandler(mockOptions) + await expect(handler.completePrompt("ping")).rejects.toThrow("Kenari completion error: boom") + }) + + it("re-throws non-Error rejections unchanged", async () => { + mockCreate.mockRejectedValue("string failure") + const handler = new KenariHandler(mockOptions) + await expect(handler.completePrompt("ping")).rejects.toBe("string failure") + }) + + it("omits temperature for models that do not support it", async () => { + vitest.mocked(getModels).mockResolvedValueOnce({ + "openai/o3-mini": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + description: "o3-mini via Kenari", + }, + }) + mockCreate.mockResolvedValue({ choices: [{ message: { content: "ok" } }] }) + + const handler = new KenariHandler({ kenariApiKey: "test-key", kenariModelId: "openai/o3-mini" }) + expect(await handler.completePrompt("ping")).toBe("ok") + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.model).toBe("openai/o3-mini") + expect("temperature" in callArgs).toBe(false) + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/kenari.spec.ts b/src/api/providers/fetchers/__tests__/kenari.spec.ts new file mode 100644 index 0000000000..41a56fa87a --- /dev/null +++ b/src/api/providers/fetchers/__tests__/kenari.spec.ts @@ -0,0 +1,146 @@ +// npx vitest run src/api/providers/fetchers/__tests__/kenari.spec.ts + +import axios from "axios" + +import { kenariDefaultModelInfo } from "@roo-code/types" + +import { getKenariModels, parseKenariModel } from "../kenari" + +vitest.mock("axios") +const mockedAxios = axios as any + +describe("Kenari Fetchers", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + describe("getKenariModels", () => { + it("maps the /models response and sends the API key as a Bearer header", async () => { + mockedAxios.get.mockResolvedValue({ + data: { + data: [ + { + id: "glm-5-2", + description: "Zhipu GLM 5.2", + context_length: 1048576, + modalities: { input: ["text"], output: ["text"] }, + }, + { + id: "claude-sonnet-5", + context_length: 1000000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + }, + ], + }, + }) + + const models = await getKenariModels("test-key") + + expect(mockedAxios.get).toHaveBeenCalledWith("https://kenari.id/v1/models", { + headers: { Authorization: "Bearer test-key" }, + timeout: 10_000, + }) + + expect(Object.keys(models).sort()).toEqual(["claude-sonnet-5", "glm-5-2"]) + expect(models["glm-5-2"]).toMatchObject({ + contextWindow: 1048576, + supportsImages: false, + supportsPromptCache: false, + description: "Zhipu GLM 5.2", + }) + expect(models["claude-sonnet-5"]).toMatchObject({ + contextWindow: 1000000, + supportsImages: true, + }) + }) + + it("falls back to default context/max tokens when metadata is absent", async () => { + mockedAxios.get.mockResolvedValue({ data: { data: [{ id: "kimi-k2-7-code" }] } }) + + const models = await getKenariModels("k") + + expect(models["kimi-k2-7-code"]).toMatchObject({ + contextWindow: kenariDefaultModelInfo.contextWindow, + maxTokens: kenariDefaultModelInfo.maxTokens, + supportsPromptCache: false, + }) + }) + + it("returns an empty map on network error", async () => { + mockedAxios.get.mockRejectedValue(new Error("network")) + expect(await getKenariModels("k")).toEqual({}) + }) + + it("falls back to an empty array when response.data.data is not an array", async () => { + mockedAxios.get.mockResolvedValue({ data: { data: null } }) + expect(await getKenariModels("k")).toEqual({}) + }) + + it("skips entries that fail safeParse with a console.warn", async () => { + mockedAxios.get.mockResolvedValue({ + data: { + data: [ + { id: "valid-model", context_window: 50000 }, + { not_a_field: true }, // no `id`, so it will fail safeParse + ], + }, + }) + const warnSpy = vitest.spyOn(console, "warn").mockImplementation(() => {}) + + const models = await getKenariModels("k") + + expect(Object.keys(models)).toEqual(["valid-model"]) + // The surviving entry keeps its mapped metadata (context_window -> contextWindow), + // so a field-mapping regression in this fallback branch is caught here. + expect(models["valid-model"].contextWindow).toBe(50000) + // Two warns: one for the outer schema mismatch, one for the invalid item + expect(warnSpy).toHaveBeenCalledTimes(2) + expect(warnSpy.mock.calls[0][0]).toContain("did not match expected schema") + expect(warnSpy.mock.calls[1][0]).toContain("Skipping invalid Kenari model entry") + + warnSpy.mockRestore() + }) + }) + + describe("parseKenariModel", () => { + it("sends no Authorization header when called without an API key", async () => { + mockedAxios.get.mockResolvedValue({ data: { data: [] } }) + + await getKenariModels() + + expect(mockedAxios.get).toHaveBeenCalledWith("https://kenari.id/v1/models", { + headers: undefined, + timeout: 10_000, + }) + }) + + it("returns an empty map on a non-Error rejection", async () => { + mockedAxios.get.mockRejectedValue("boom") + expect(await getKenariModels("k")).toEqual({}) + }) + + it("falls back to the model name when no description is provided", () => { + const info = parseKenariModel({ id: "x", name: "Model X" }) + expect(info.description).toBe("Model X") + }) + + it("prefers context_window over context_length and max_output_tokens over max_tokens", () => { + const info = parseKenariModel({ + id: "dual", + context_window: 111, + context_length: 222, + max_output_tokens: 33, + max_tokens: 44, + }) + expect(info.contextWindow).toBe(111) + expect(info.maxTokens).toBe(33) + }) + + it("treats a model with no cache pricing as not cache-capable", () => { + const info = parseKenariModel({ id: "x", context_window: 100000, max_tokens: 8000 }) + expect(info.supportsPromptCache).toBe(false) + expect(info.contextWindow).toBe(100000) + expect(info.maxTokens).toBe(8000) + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 5b771f2bda..4b851d7d1f 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -43,6 +43,7 @@ vi.mock("fs", () => ({ vi.mock("../litellm") vi.mock("../openrouter") vi.mock("../requesty") +vi.mock("../kenari") // Mock ContextProxy with a simple static instance vi.mock("../../../core/config/ContextProxy", () => ({ @@ -63,10 +64,12 @@ import { getModels, getModelsFromCache } from "../modelCache" import { getLiteLLMModels } from "../litellm" import { getOpenRouterModels } from "../openrouter" import { getRequestyModels } from "../requesty" +import { getKenariModels } from "../kenari" const mockGetLiteLLMModels = getLiteLLMModels as Mock const mockGetOpenRouterModels = getOpenRouterModels as Mock const mockGetRequestyModels = getRequestyModels as Mock +const mockGetKenariModels = getKenariModels as Mock const DUMMY_REQUESTY_KEY = "requesty-key-for-testing" @@ -130,6 +133,23 @@ describe("getModels with new GetModelsOptions", () => { expect(result).toEqual(mockModels) }) + it("calls getKenariModels with optional API key", async () => { + const mockModels = { + "glm-5-2": { + maxTokens: 32768, + contextWindow: 1048576, + supportsPromptCache: false, + description: "GLM 5.2 via Kenari", + }, + } + mockGetKenariModels.mockResolvedValue(mockModels) + + const result = await getModels({ provider: "kenari", apiKey: "kenari-key-for-testing" }) + + expect(mockGetKenariModels).toHaveBeenCalledWith("kenari-key-for-testing") + expect(result).toEqual(mockModels) + }) + it("handles errors and re-throws them", async () => { const expectedError = new Error("LiteLLM connection failed") mockGetLiteLLMModels.mockRejectedValue(expectedError) diff --git a/src/api/providers/fetchers/kenari.ts b/src/api/providers/fetchers/kenari.ts new file mode 100644 index 0000000000..b0c69c4efa --- /dev/null +++ b/src/api/providers/fetchers/kenari.ts @@ -0,0 +1,98 @@ +import axios from "axios" +import { z } from "zod" + +import type { ModelInfo } from "@roo-code/types" +import { kenariDefaultModelInfo, KENARI_BASE_URL } from "@roo-code/types" + +// The Kenari `/models` endpoint follows the OpenAI `/models` shape and is +// public (no key required). The `id` is the only guaranteed field; metadata is +// optional and best-effort, so the schema is intentionally permissive. +// Pricing is intentionally NOT parsed: Kenari returns prices in IDR +// (`micro_idr_per_1m_tokens`), and the ModelInfo price fields are USD per 1M +// tokens, so reporting a converted or raw value would be wrong. Cost stays +// undefined instead. +const kenariModelSchema = z.object({ + id: z.string(), + name: z.string().optional(), + description: z.string().optional(), + context_window: z.number().optional(), + context_length: z.number().optional(), + max_tokens: z.number().optional(), + max_output_tokens: z.number().optional(), + modalities: z + .object({ + input: z.array(z.string()).optional(), + output: z.array(z.string()).optional(), + }) + .optional(), +}) + +export type KenariModel = z.infer + +const kenariModelsResponseSchema = z.object({ + data: z.array(kenariModelSchema), +}) + +/** + * Maps a raw Kenari model entry to the internal {@link ModelInfo} shape. + * + * Falls back to {@link kenariDefaultModelInfo} when the upstream payload + * omits context-window or max-token fields, ensuring downstream consumers + * always receive a fully-populated object. + * + * @param model - Validated model entry from the `/models` response. + * @returns Normalised model metadata suitable for the model picker. + */ +export const parseKenariModel = (model: KenariModel): ModelInfo => ({ + maxTokens: model.max_output_tokens ?? model.max_tokens ?? kenariDefaultModelInfo.maxTokens, + contextWindow: model.context_window ?? model.context_length ?? kenariDefaultModelInfo.contextWindow, + supportsImages: model.modalities?.input?.includes("image") ?? false, + supportsPromptCache: false, + description: model.description ?? model.name, +}) + +/** + * Fetches the list of available models from the Kenari `/models` endpoint. + * + * The endpoint shape mirrors the OpenAI `/models` response. A permissive Zod + * schema is used so that unknown fields are silently dropped rather than + * causing a hard failure. Invalid entries (e.g. missing `id`) are skipped + * with a console warning rather than propagated to the UI. + * + * @param apiKey - Optional Bearer token; the endpoint is public but the key is + * sent when available. + * @returns A record mapping model IDs to their normalised {@link ModelInfo}. + */ +export async function getKenariModels(apiKey?: string): Promise> { + const models: Record = {} + + try { + const response = await axios.get(`${KENARI_BASE_URL}/models`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, + timeout: 10_000, + }) + + const result = kenariModelsResponseSchema.safeParse(response.data) + const rawData = result.success ? result.data.data : response.data?.data + const data = Array.isArray(rawData) ? rawData : [] + + if (!result.success) { + console.warn( + `Kenari models response did not match expected schema; falling back to per-item parsing: ${JSON.stringify(result.error.format())}`, + ) + } + + for (const rawModel of data) { + const parsed = kenariModelSchema.safeParse(rawModel) + if (!parsed.success) { + console.warn(`Skipping invalid Kenari model entry: ${JSON.stringify(rawModel)}`) + continue + } + models[parsed.data.id] = parseKenariModel(parsed.data) + } + } catch (error) { + console.error(`Error fetching Kenari models: ${error instanceof Error ? error.message : String(error)}`) + } + + return models +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 312f4f9382..deca6a7c00 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -20,6 +20,7 @@ import { fileExistsAtPath } from "../../../utils/fs" import { getOpenRouterModels } from "./openrouter" import { getVercelAiGatewayModels } from "./vercel-ai-gateway" import { getOpencodeGoModels } from "./opencode-go" +import { getKenariModels } from "./kenari" import { getRequestyModels } from "./requesty" import { getUnboundModels } from "./unbound" import { getLiteLLMModels } from "./litellm" @@ -211,6 +212,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + const { id: modelId, info } = await this.fetchModel() + + try { + const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + stream: false, + } + + if (this.supportsTemperature(modelId)) { + requestOptions.temperature = this.options.modelTemperature ?? KENARI_DEFAULT_TEMPERATURE + } + + requestOptions.max_completion_tokens = info.maxTokens + + const response = await this.client.chat.completions.create(requestOptions) + return response.choices[0]?.message.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(`Kenari completion error: ${error.message}`) + } + throw error + } + } +} diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 1904b46bd4..b1e5df93f9 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2686,6 +2686,8 @@ describe("ClineProvider - Router Models", () => { }) // Opencode Go's /models endpoint is public, so it is fetched like the other no-auth routers. expect(getModels).toHaveBeenCalledWith(expect.objectContaining({ provider: "opencode-go" })) + // Kenari's /models endpoint is public, so it is fetched like the other no-auth routers. + expect(getModels).toHaveBeenCalledWith(expect.objectContaining({ provider: "kenari" })) // Verify response was sent expect(mockPostMessage).toHaveBeenCalledWith({ @@ -2702,6 +2704,7 @@ describe("ClineProvider - Router Models", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) @@ -2734,6 +2737,7 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // zoo-gateway success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail .mockResolvedValueOnce(mockModels) // opencode-go (public endpoint) + .mockResolvedValueOnce(mockModels) // kenari (public endpoint) await messageHandler({ type: "requestRouterModels" }) @@ -2752,6 +2756,7 @@ describe("ClineProvider - Router Models", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) @@ -2849,6 +2854,7 @@ describe("ClineProvider - Router Models", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 335911a7c7..12832da249 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -384,6 +384,8 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) // Opencode Go's /models endpoint is public, so it is fetched like the other no-auth routers. expect(mockGetModels).toHaveBeenCalledWith(expect.objectContaining({ provider: "opencode-go" })) + // Kenari's /models endpoint is public, so it is fetched like the other no-auth routers. + expect(mockGetModels).toHaveBeenCalledWith(expect.objectContaining({ provider: "kenari" })) // Verify response was sent expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -400,6 +402,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) @@ -468,6 +471,40 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) }) + it("flushes and fetches Kenari models when an explicit API key is supplied", async () => { + mockClineProvider.getState = vi.fn().mockResolvedValue({ + apiConfiguration: {}, + }) + mockGetModels.mockResolvedValue({ + "glm-5-2": { + maxTokens: 32768, + contextWindow: 1048576, + supportsPromptCache: false, + description: "Kenari model", + }, + }) + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + values: { + provider: "kenari", + kenariApiKey: "fresh-kenari-key", + }, + }) + + expect(mockFlushModels).toHaveBeenCalledWith({ provider: "kenari", apiKey: "fresh-kenari-key" }, true) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "kenari", apiKey: "fresh-kenari-key" }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "routerModels", + routerModels: { + kenari: { + "glm-5-2": expect.objectContaining({ description: "Kenari model" }), + }, + }, + values: { provider: "kenari" }, + }) + }) + it("handles LiteLLM models with values from message when config is missing", async () => { mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { @@ -551,6 +588,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) @@ -610,6 +648,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { poe: {}, deepseek: {}, "opencode-go": mockModels, + kenari: mockModels, }, values: undefined, }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index fdcc53ddc1..01abc5db98 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1042,6 +1042,7 @@ export const webviewMessageHandler = async ( poe: {}, deepseek: {}, "opencode-go": {}, + kenari: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1152,6 +1153,23 @@ export const webviewMessageHandler = async ( options: { provider: "opencode-go", apiKey: opencodeGoApiKey }, }) + // Kenari's /models endpoint is public — it returns the full model list with no + // Authorization header — so it's fetched unconditionally like openrouter/vercel-ai-gateway + // above. Gating it behind a key meant the picker stayed empty (and fell back to the default + // model) whenever the key wasn't yet in apiConfiguration at fetch time. The key is still + // forwarded when present. + const kenariApiKey = message?.values?.kenariApiKey ?? apiConfiguration.kenariApiKey + + // Refresh the cache when a new key is explicitly provided (e.g. the Refresh Models button). + if (message?.values?.kenariApiKey) { + await flushModels({ provider: "kenari", apiKey: kenariApiKey }, true) + } + + candidates.push({ + key: "kenari", + options: { provider: "kenari", apiKey: kenariApiKey }, + }) + // Apply single provider filter if specified const modelFetchPromises = providerFilter ? candidates.filter(({ key }) => key === providerFilter) diff --git a/src/shared/api.ts b/src/shared/api.ts index fe2042bd5f..d5a579be9d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -187,6 +187,7 @@ const dynamicProviderExtras = { poe: {} as { apiKey?: string; baseUrl?: string }, deepseek: {} as { apiKey?: string; baseUrl?: string }, "opencode-go": {} as { apiKey?: string }, + kenari: {} as { apiKey?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 6bfe20c718..2585a674ec 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -74,6 +74,7 @@ import { Friendli, VercelAiGateway, OpenCodeGo, + Kenari, ZooGateway, MiniMax, Mimo, @@ -646,6 +647,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === "kenari" && ( + + )} + {selectedProvider === "zoo-gateway" && ( ({ ), })) +// Mock Kenari provider for tests +vi.mock("../providers/Kenari", () => ({ + Kenari: ({ apiConfiguration, setApiConfigurationField }: any) => ( +
+ setApiConfigurationField("kenariApiKey", e.target.value)} + placeholder="API Key" + /> +
+ ), +})) + // Mock Friendli provider for tests vi.mock("../providers/Friendli", () => ({ Friendli: ({ apiConfiguration }: any) => ( @@ -615,6 +630,30 @@ describe("ApiOptions", () => { }) }) + describe("Kenari provider tests", () => { + it("renders Kenari component when provider is selected", () => { + renderApiOptions({ + apiConfiguration: { + apiProvider: "kenari", + kenariApiKey: "kn-test-key", + }, + }) + + expect(screen.getByTestId("kenari-provider")).toBeInTheDocument() + expect(screen.getByTestId("kenari-api-key")).toHaveValue("kn-test-key") + }) + + it("does not render Kenari component when other provider is selected", () => { + renderApiOptions({ + apiConfiguration: { + apiProvider: "anthropic", + }, + }) + + expect(screen.queryByTestId("kenari-provider")).not.toBeInTheDocument() + }) + }) + it("renders retired provider message and hides provider-specific forms", () => { renderApiOptions({ apiConfiguration: { diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 333c32e085..aa259b0efe 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -67,6 +67,7 @@ export const PROVIDERS = [ { value: "friendli", label: "Friendli", proxy: false }, { value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false }, { value: "opencode-go", label: "Opencode Go", proxy: false }, + { value: "kenari", label: "Kenari", proxy: false }, { value: "zoo-gateway", label: "Zoo Gateway", proxy: false }, { value: "minimax", label: "MiniMax", proxy: false }, { value: "mimo", label: "Xiaomi MiMo", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/Kenari.tsx b/webview-ui/src/components/settings/providers/Kenari.tsx new file mode 100644 index 0000000000..577a44ac71 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Kenari.tsx @@ -0,0 +1,82 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { + type ProviderSettings, + type OrganizationAllowList, + type RouterModels, + kenariDefaultModelId, +} from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" + +type KenariProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const Kenari = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: KenariProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.kenariApiKey && ( + + {t("settings:providers.getKenariApiKey")} + + )} + + + ) +} diff --git a/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx new file mode 100644 index 0000000000..e9fb7ce34c --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx @@ -0,0 +1,90 @@ +import { render, screen, fireEvent } from "@testing-library/react" + +import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types" +import { kenariDefaultModelId } from "@roo-code/types" + +import { Kenari } from "../Kenari" + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ children, value, onInput, type }: any) => ( +
+ {children} + onInput(e)} data-testid="api-key-input" /> +
+ ), +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@src/components/common/VSCodeButtonLink", () => ({ + VSCodeButtonLink: ({ children, href }: any) => ( + + {children} + + ), +})) + +// Stub ModelPicker so we can assert the props it receives without pulling in its hooks. +vi.mock("../../ModelPicker", () => ({ + ModelPicker: ({ defaultModelId, modelIdKey, serviceName }: any) => ( +
+ ), +})) + +describe("Kenari", () => { + const organizationAllowList: OrganizationAllowList = { allowAll: true, providers: {} } + const mockSetApiConfigurationField = vi.fn() + + const renderComponent = (apiConfiguration: ProviderSettings) => + render( + , + ) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("updates the API key via setApiConfigurationField on input", () => { + renderComponent({ kenariApiKey: "" }) + + fireEvent.change(screen.getByTestId("api-key-input"), { target: { value: "secret-key" } }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("kenariApiKey", "secret-key") + }) + + it("shows the get-API-key CTA only when no API key is set", () => { + const { rerender } = renderComponent({ kenariApiKey: "" }) + const link = screen.getByTestId("get-api-key-link") + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute("href", "https://kenari.id/login?next=/keys") + + rerender( + , + ) + expect(screen.queryByTestId("get-api-key-link")).not.toBeInTheDocument() + }) + + it("wires the ModelPicker with the Kenari defaults", () => { + renderComponent({ kenariApiKey: "key" }) + + const picker = screen.getByTestId("model-picker") + expect(picker).toHaveAttribute("data-default-model-id", kenariDefaultModelId) + expect(picker).toHaveAttribute("data-model-id-key", "kenariModelId") + expect(picker).toHaveAttribute("data-service-name", "Kenari") + }) +}) diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 211f51526b..6a990c37a5 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -24,6 +24,7 @@ export { Fireworks } from "./Fireworks" export { Friendli } from "./Friendli" export { VercelAiGateway } from "./VercelAiGateway" export { OpenCodeGo } from "./OpenCodeGo" +export { Kenari } from "./Kenari" export { ZooGateway } from "./ZooGateway" export { MiniMax } from "./MiniMax" export { Mimo } from "./Mimo" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index e5009995ec..d4c87ec02c 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -26,6 +26,7 @@ import { litellmDefaultModelId, vercelAiGatewayDefaultModelId, opencodeGoDefaultModelId, + kenariDefaultModelId, zooGatewayDefaultModelId, } from "@roo-code/types" @@ -130,6 +131,7 @@ const PROVIDER_MODEL_CONFIG: Partial> poe: { field: "apiModelId", default: poeDefaultModelId }, "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId }, "opencode-go": { field: "opencodeGoModelId", default: opencodeGoDefaultModelId }, + kenari: { field: "kenariModelId", default: kenariDefaultModelId }, "zoo-gateway": { field: "zooGatewayModelId", default: zooGatewayDefaultModelId }, openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 5121946764..9f107b3841 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -10,6 +10,8 @@ import { ModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, litellmDefaultModelInfo, + kenariDefaultModelId, + kenariDefaultModelInfo, openAiModelInfoSaneDefaults, minimaxDefaultModelId, minimaxModels, @@ -722,6 +724,78 @@ describe("useSelectedModel", () => { }) }) + describe("kenari provider", () => { + beforeEach(() => { + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + }) + + it("should return routerModels info for the configured kenari model", () => { + const customModelInfo: ModelInfo = { + maxTokens: 32768, + contextWindow: 1048576, + supportsImages: false, + supportsPromptCache: false, + description: "GLM 5.2 via Kenari", + } + + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: { + "glm-5-2": customModelInfo, + }, + }, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "kenari", + kenariModelId: "glm-5-2", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("kenari") + expect(result.current.id).toBe("glm-5-2") + expect(result.current.info).toEqual(customModelInfo) + }) + + it("should use kenariDefaultModelInfo as fallback when routerModels.kenari is empty", () => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: {}, + }, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "kenari", + kenariModelId: "some-model", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("kenari") + // Falls back to the kenari default model ID when the router list is empty + expect(result.current.id).toBe(kenariDefaultModelId) + // Should use kenariDefaultModelInfo as fallback + expect(result.current.info).toEqual(kenariDefaultModelInfo) + }) + }) + describe("openai provider", () => { beforeEach(() => { mockUseRouterModels.mockReturnValue({ diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 2d6963bc65..62a30a26b2 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -29,6 +29,7 @@ import { litellmDefaultModelInfo, lMStudioDefaultModelInfo, opencodeGoDefaultModelInfo, + kenariDefaultModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, VERTEX_1M_CONTEXT_MODEL_IDS, isDynamicProvider, @@ -384,6 +385,13 @@ function getSelectedModel({ const info = routerModels["opencode-go"]?.[id] ?? opencodeGoDefaultModelInfo return { id, info } } + case "kenari": { + const id = getValidatedModelId(apiConfiguration.kenariModelId, routerModels["kenari"], defaultModelId) + // Fall back to the provider's default ModelInfo so capability-driven UI + // keeps working when the /models list is empty or unavailable. + const info = routerModels["kenari"]?.[id] ?? kenariDefaultModelInfo + return { id, info } + } case "zoo-gateway": { const id = getValidatedModelId( apiConfiguration.zooGatewayModelId, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 2f390b3213..4cb1e90027 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Obtenir clau API de Vercel AI Gateway", "opencodeGoApiKey": "Clau API de Opencode Go", "getOpencodeGoApiKey": "Obtenir clau API de Opencode Go", + "kenariApiKey": "Clau API de Kenari", + "getKenariApiKey": "Obtenir clau API de Kenari", "apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 76a492a067..8566410041 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel erhalten", "opencodeGoApiKey": "Opencode Go API-Schlüssel", "getOpencodeGoApiKey": "Opencode Go API-Schlüssel erhalten", + "kenariApiKey": "Kenari API-Schlüssel", + "getKenariApiKey": "Kenari API-Schlüssel erhalten", "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 72c78bf065..9ef634f866 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -462,6 +462,8 @@ "getVercelAiGatewayApiKey": "Get Vercel AI Gateway API Key", "opencodeGoApiKey": "Opencode Go API Key", "getOpencodeGoApiKey": "Get Opencode Go API Key", + "kenariApiKey": "Kenari API Key", + "getKenariApiKey": "Get Kenari API Key", "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 7de02e6cf6..7500e5bc77 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Obtener clave API de Vercel AI Gateway", "opencodeGoApiKey": "Clave API de Opencode Go", "getOpencodeGoApiKey": "Obtener clave API de Opencode Go", + "kenariApiKey": "Clave API de Kenari", + "getKenariApiKey": "Obtener clave API de Kenari", "apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 13dbb025ce..9d5178f90f 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Obtenir la clé API Vercel AI Gateway", "opencodeGoApiKey": "Clé API Opencode Go", "getOpencodeGoApiKey": "Obtenir la clé API Opencode Go", + "kenariApiKey": "Clé API Kenari", + "getKenariApiKey": "Obtenir la clé API Kenari", "apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 1f155c9740..2072485021 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API कुंजी प्राप्त करें", "opencodeGoApiKey": "Opencode Go API कुंजी", "getOpencodeGoApiKey": "Opencode Go API कुंजी प्राप्त करें", + "kenariApiKey": "Kenari API कुंजी", + "getKenariApiKey": "Kenari API कुंजी प्राप्त करें", "apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 5c90f85103..0b30cb84f4 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Dapatkan Vercel AI Gateway API Key", "opencodeGoApiKey": "Opencode Go API Key", "getOpencodeGoApiKey": "Dapatkan Opencode Go API Key", + "kenariApiKey": "Kenari API Key", + "getKenariApiKey": "Dapatkan Kenari API Key", "apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 50bb0e20d6..125e6a725f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Ottieni chiave API Vercel AI Gateway", "opencodeGoApiKey": "Chiave API Opencode Go", "getOpencodeGoApiKey": "Ottieni chiave API Opencode Go", + "kenariApiKey": "Chiave API Kenari", + "getKenariApiKey": "Ottieni chiave API Kenari", "apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 12455c8b9d..dd07efdd9a 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway APIキーを取得", "opencodeGoApiKey": "Opencode Go APIキー", "getOpencodeGoApiKey": "Opencode Go APIキーを取得", + "kenariApiKey": "Kenari APIキー", + "getKenariApiKey": "Kenari APIキーを取得", "apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 91b84dd41b..36a4ee14e0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API 키 받기", "opencodeGoApiKey": "Opencode Go API 키", "getOpencodeGoApiKey": "Opencode Go API 키 받기", + "kenariApiKey": "Kenari API 키", + "getKenariApiKey": "Kenari API 키 받기", "apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index add5237373..5c36d91e30 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel ophalen", "opencodeGoApiKey": "Opencode Go API-sleutel", "getOpencodeGoApiKey": "Opencode Go API-sleutel ophalen", + "kenariApiKey": "Kenari API-sleutel", + "getKenariApiKey": "Kenari API-sleutel ophalen", "apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 021b8a6075..9f9da60b68 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Uzyskaj klucz API Vercel AI Gateway", "opencodeGoApiKey": "Klucz API Opencode Go", "getOpencodeGoApiKey": "Uzyskaj klucz API Opencode Go", + "kenariApiKey": "Klucz API Kenari", + "getKenariApiKey": "Uzyskaj klucz API Kenari", "apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 15203c1d31..b267f41573 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Obter chave API do Vercel AI Gateway", "opencodeGoApiKey": "Chave API do Opencode Go", "getOpencodeGoApiKey": "Obter chave API do Opencode Go", + "kenariApiKey": "Chave API do Kenari", + "getKenariApiKey": "Obter chave API do Kenari", "apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index bc2b7b4398..2d427202f9 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Получить ключ API Vercel AI Gateway", "opencodeGoApiKey": "Ключ API Opencode Go", "getOpencodeGoApiKey": "Получить ключ API Opencode Go", + "kenariApiKey": "Ключ API Kenari", + "getKenariApiKey": "Получить ключ API Kenari", "apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 0f831af080..ba4b092cf3 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı Al", "opencodeGoApiKey": "Opencode Go API Anahtarı", "getOpencodeGoApiKey": "Opencode Go API Anahtarı Al", + "kenariApiKey": "Kenari API Anahtarı", + "getKenariApiKey": "Kenari API Anahtarı Al", "apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 424f17734a..56a04f6c59 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "Lấy khóa API Vercel AI Gateway", "opencodeGoApiKey": "Khóa API Opencode Go", "getOpencodeGoApiKey": "Lấy khóa API Opencode Go", + "kenariApiKey": "Khóa API Kenari", + "getKenariApiKey": "Lấy khóa API Kenari", "apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 4e677eb329..03fc075da9 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -382,6 +382,8 @@ "getVercelAiGatewayApiKey": "获取 Vercel AI Gateway API 密钥", "opencodeGoApiKey": "Opencode Go API 密钥", "getOpencodeGoApiKey": "获取 Opencode Go API 密钥", + "kenariApiKey": "Kenari API 密钥", + "getKenariApiKey": "获取 Kenari API 密钥", "apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 34e7001b86..b84cb627e7 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -409,6 +409,8 @@ "getVercelAiGatewayApiKey": "取得 Vercel AI Gateway API 金鑰", "opencodeGoApiKey": "Opencode Go API 金鑰", "getOpencodeGoApiKey": "取得 Opencode Go API 金鑰", + "kenariApiKey": "Kenari API 金鑰", + "getKenariApiKey": "取得 Kenari API 金鑰", "apiKeyStorageNotice": "API 金鑰會安全地儲存在 VS Code 的 Secret Storage 中", "openAiCodexRateLimits": { "title": "Codex 用量限制{{planLabel}}", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 453be4547d..04484397c6 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -52,6 +52,7 @@ describe("Model Validation Functions", () => { poe: {}, deepseek: {}, "opencode-go": {}, + kenari: {}, "zoo-gateway": {}, } @@ -216,6 +217,40 @@ describe("Model Validation Functions", () => { expect(result).toBe("settings:validation.modelId") }) }) + describe("Kenari validation", () => { + it("returns an apiKey error when the Kenari API key is missing", () => { + const config: ProviderSettings = { + apiProvider: "kenari", + kenariModelId: "glm-5.1", + // Missing kenariApiKey + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBe("settings:validation.apiKey") + }) + + it("returns undefined for a valid Kenari configuration", () => { + const config: ProviderSettings = { + apiProvider: "kenari", + kenariApiKey: "valid-key", + kenariModelId: "glm-5.1", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBeUndefined() + }) + + it("returns a modelId error when no Kenari model id is set", () => { + const config: ProviderSettings = { + apiProvider: "kenari", + kenariApiKey: "valid-key", + // Missing kenariModelId + } + + const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + expect(result).toBe("settings:validation.modelId") + }) + }) describe("Friendli validation", () => { it("returns an apiKey error when the Friendli API key is missing", () => { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index c19839672f..d780786808 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -137,6 +137,11 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.apiKey") } break + case "kenari": + if (!apiConfiguration.kenariApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case "zoo-gateway": if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { return i18next.t("settings:validation.zooGatewaySignIn") From c9ac484e5e268a2ce6c7453ed74f78f5cf895f4a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:02:02 +0000 Subject: [PATCH 12/89] chore(deps): update dependency tsup to v8.5.1 (#813) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- packages/types/package.json | 2 +- pnpm-lock.yaml | 42 +++++++------------------------------ 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 15b932aeb7..c720b1842f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -45,7 +45,7 @@ "@vitest/coverage-v8": "4.1.9", "ink-testing-library": "4.0.0", "rimraf": "6.0.1", - "tsup": "8.5.0", + "tsup": "8.5.1", "tsx": "4.22.4", "vitest": "4.1.9" } diff --git a/packages/types/package.json b/packages/types/package.json index 77a8415f86..32174d2cc5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -32,7 +32,7 @@ "@roo-code/config-typescript": "workspace:^", "@types/node": "20.19.43", "globals": "16.3.0", - "tsup": "8.5.0", + "tsup": "8.5.1", "ajv": "8.20.0", "vitest": "4.1.9", "zod-to-json-schema": "3.25.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7ecdc7b7..f35270cf16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,8 +141,8 @@ importers: specifier: 6.0.1 version: 6.0.1 tsup: - specifier: 8.5.0 - version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + specifier: 8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) tsx: specifier: 4.22.4 version: 4.22.4 @@ -395,8 +395,8 @@ importers: specifier: 16.3.0 version: 16.3.0 tsup: - specifier: 8.5.0 - version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + specifier: 8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -6087,9 +6087,6 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -7943,9 +7940,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -8000,8 +7994,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.0: - resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -8420,9 +8414,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -8446,9 +8437,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -14381,8 +14369,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} lodash.truncate@4.4.2: {} @@ -16737,10 +16723,6 @@ snapshots: tr46@0.0.3: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -16779,7 +16761,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 @@ -16793,7 +16775,7 @@ snapshots: postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.60.4 - source-map: 0.8.0-beta.0 + source-map: 0.7.6 sucrase: 3.35.0 tinyexec: 0.3.2 tinyglobby: 0.2.17 @@ -17192,8 +17174,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: @@ -17214,12 +17194,6 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 From 05666077bd98d2af5f567d8483f4e9720a1582cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:07:34 -0400 Subject: [PATCH 13/89] fix(deps): update dependency @vscode/codicons to ^0.0.45 (#817) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++++----- webview-ui/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f35270cf16..0710f85c9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -747,8 +747,8 @@ importers: specifier: ^5.68.0 version: 5.76.1(react@18.3.1) '@vscode/codicons': - specifier: ^0.0.36 - version: 0.0.36 + specifier: ^0.0.45 + version: 0.0.45 '@vscode/webview-ui-toolkit': specifier: ^1.4.0 version: 1.4.0(react@18.3.1) @@ -3471,8 +3471,8 @@ packages: '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} - '@vscode/codicons@0.0.36': - resolution: {integrity: sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==} + '@vscode/codicons@0.0.45': + resolution: {integrity: sha512-1KAZ7XCMagp5Gdrlr4bbbcAqgcIL623iO1wW6rfcSVGAVUQvR0WP7bQx1SbJ11gmV3fdQTSEFIJQ/5C+HuVasw==} '@vscode/ripgrep@1.17.0': resolution: {integrity: sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==} @@ -11459,7 +11459,7 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@vscode/codicons@0.0.36': {} + '@vscode/codicons@0.0.45': {} '@vscode/ripgrep@1.17.0': dependencies: diff --git a/webview-ui/package.json b/webview-ui/package.json index 6a90a04c0f..13f12626b5 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -34,7 +34,7 @@ "@roo-code/types": "workspace:^", "@tailwindcss/vite": "^4.0.0", "@tanstack/react-query": "^5.68.0", - "@vscode/codicons": "^0.0.36", + "@vscode/codicons": "^0.0.45", "@vscode/webview-ui-toolkit": "^1.4.0", "ansi-to-html": "^0.7.2", "axios": "^1.12.0", From b29af3b862e4efbcf5ea7197e0db0297da3e50e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:10:35 +0000 Subject: [PATCH 14/89] chore(deps): update dependency zod-to-json-schema to v3.25.2 (#814) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/types/package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index 32174d2cc5..0f5cf6187b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -35,6 +35,6 @@ "tsup": "8.5.1", "ajv": "8.20.0", "vitest": "4.1.9", - "zod-to-json-schema": "3.25.1" + "zod-to-json-schema": "3.25.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0710f85c9d..17bf1eab1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -401,8 +401,8 @@ importers: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) zod-to-json-schema: - specifier: 3.25.1 - version: 3.25.1(zod@3.25.76) + specifier: 3.25.2 + version: 3.25.2(zod@3.25.76) packages/vscode-shim: devDependencies: @@ -8612,8 +8612,8 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: zod: 3.25.76 @@ -9714,7 +9714,7 @@ snapshots: chalk: 4.1.2 jsonschema: 1.5.0 zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9766,7 +9766,7 @@ snapshots: dependencies: ws: 8.21.0 zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9789,7 +9789,7 @@ snapshots: pkce-challenge: 5.0.0 raw-body: 3.0.2 zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -17377,7 +17377,7 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-to-json-schema@3.25.1(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 From b3bfdc633e7758863e445e786982021b301339ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:47:16 -0400 Subject: [PATCH 15/89] chore(deps): update dependency @changesets/cli to v2.31.0 (#819) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 83 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 707521afc8..125ecc7c01 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "npm:publish:types": "pnpm --filter @roo-code/types npm:publish" }, "devDependencies": { - "@changesets/cli": "2.29.7", + "@changesets/cli": "2.31.0", "@dotenvx/dotenvx": "1.66.0", "@roo-code/config-typescript": "workspace:^", "@types/node": "20.19.43", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17bf1eab1b..20632ccd88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: .: devDependencies: '@changesets/cli': - specifier: 2.29.7 - version: 2.29.7(@types/node@20.19.43) + specifier: 2.31.0 + version: 2.31.0(@types/node@20.19.43) '@dotenvx/dotenvx': specifier: 1.66.0 version: 1.66.0 @@ -1284,30 +1284,30 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@changesets/apply-release-plan@7.0.13': - resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} - '@changesets/assemble-release-plan@6.0.9': - resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.7': - resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} hasBin: true - '@changesets/config@3.1.1': - resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} - '@changesets/get-release-plan@4.0.13': - resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -1318,14 +1318,14 @@ packages: '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - '@changesets/parse@0.4.1': - resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} '@changesets/pre@2.0.2': resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - '@changesets/read@0.6.5': - resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} '@changesets/should-skip-package@0.1.2': resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} @@ -9207,9 +9207,9 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} - '@changesets/apply-release-plan@7.0.13': + '@changesets/apply-release-plan@7.1.1': dependencies: - '@changesets/config': 3.1.1 + '@changesets/config': 3.1.4 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 @@ -9223,10 +9223,10 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.3 - '@changesets/assemble-release-plan@6.0.9': + '@changesets/assemble-release-plan@6.0.10': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -9236,30 +9236,28 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.7(@types/node@20.19.43)': + '@changesets/cli@2.31.0(@types/node@20.19.43)': dependencies: - '@changesets/apply-release-plan': 7.0.13 - '@changesets/assemble-release-plan': 6.0.9 + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.1 + '@changesets/config': 3.1.4 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.13 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 + '@changesets/read': 0.6.7 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 '@inquirer/external-editor': 1.0.2(@types/node@20.19.43) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 - ci-info: 3.9.0 enquirer: 2.4.1 fs-extra: 7.0.1 mri: 1.2.0 - p-limit: 2.3.0 package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 @@ -9269,11 +9267,12 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@changesets/config@3.1.1': + '@changesets/config@3.1.4': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 @@ -9283,19 +9282,19 @@ snapshots: dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.3': + '@changesets/get-dependents-graph@2.1.4': dependencies: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 semver: 7.7.3 - '@changesets/get-release-plan@4.0.13': + '@changesets/get-release-plan@4.0.16': dependencies: - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 + '@changesets/read': 0.6.7 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -9313,10 +9312,10 @@ snapshots: dependencies: picocolors: 1.1.1 - '@changesets/parse@0.4.1': + '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 - js-yaml: 3.14.1 + js-yaml: 4.2.0 '@changesets/pre@2.0.2': dependencies: @@ -9325,11 +9324,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.5': + '@changesets/read@0.6.7': dependencies: '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.1 + '@changesets/parse': 0.4.3 '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 From 042a3ab474b85119e436dd0d2af2a0d515d43b98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:54:31 +0000 Subject: [PATCH 16/89] fix(deps): update ai sdks and providers (#816) * fix(deps): update ai sdks and providers * fix(deps): bump google-auth-library to ^10.2.0 for vertex-sdk 0.19.0 compat --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- pnpm-lock.yaml | 764 ++++++++++++++++++----------------------------- src/package.json | 10 +- 2 files changed, 290 insertions(+), 484 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20632ccd88..081ac8817e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -374,7 +374,7 @@ importers: dependencies: ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@6.0.218(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -422,17 +422,17 @@ importers: src: dependencies: '@anthropic-ai/sdk': - specifier: ^0.106.0 - version: 0.106.0(zod@3.25.76) + specifier: ^0.109.0 + version: 0.109.1(zod@3.25.76) '@anthropic-ai/vertex-sdk': - specifier: ^0.18.0 - version: 0.18.0(zod@3.25.76) + specifier: ^0.19.0 + version: 0.19.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': specifier: ^3.922.0 - version: 3.1075.0 + version: 3.1078.0 '@aws-sdk/credential-providers': specifier: ^3.922.0 - version: 3.1075.0 + version: 3.1078.0 '@google/genai': specifier: ^1.29.1 version: 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)) @@ -465,7 +465,7 @@ importers: version: link:../packages/types ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@6.0.218(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -503,8 +503,8 @@ importers: specifier: ^3.0.0 version: 3.0.0 google-auth-library: - specifier: ^9.15.1 - version: 9.15.1 + specifier: ^10.2.0 + version: 10.9.0 gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -618,8 +618,8 @@ importers: version: 3.25.76 devDependencies: '@ai-sdk/openai-compatible': - specifier: 2.0.51 - version: 2.0.51(zod@3.25.76) + specifier: 2.0.56 + version: 2.0.56(zod@3.25.76) '@roo-code/build': specifier: workspace:^ version: link:../packages/build @@ -666,8 +666,8 @@ importers: specifier: 3.9.2 version: 3.9.2 ai: - specifier: 6.0.209 - version: 6.0.209(zod@3.25.76) + specifier: 6.0.218 + version: 6.0.218(zod@3.25.76) esbuild-wasm: specifier: 0.25.12 version: 0.25.12 @@ -856,7 +856,7 @@ importers: version: 3.4.1 source-map: specifier: ^0.7.4 - version: 0.7.6 + version: 0.7.4 stacktrace-js: specifier: ^2.0.2 version: 2.0.2 @@ -963,14 +963,14 @@ packages: peerDependencies: zod: 3.25.76 - '@ai-sdk/gateway@3.0.134': - resolution: {integrity: sha512-PfUkQp01EihEyNM6e+nexXmVANiY5KcHMui/MmaHsC4KVVQYh/MwPeOsD3bTPu63e581BKO+AwaPPNmD4x943A==} + '@ai-sdk/gateway@3.0.142': + resolution: {integrity: sha512-Y1iwdxdebYXpoK5y/4CrcCfJGeFwEJWlEx+pMWIg/ZVWGi9KA+JXM7YBkhxcshpq/jAXx8YyQeFMyZr0TGfXZQ==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/openai-compatible@2.0.51': - resolution: {integrity: sha512-A6qfyaVs4lxmRxRux6U3ViOa8mMbsSd0OaHghpei2MpiBT6791J4zFH5MN7kaW1tLfQ246rSC2DVTMOavsycjQ==} + '@ai-sdk/openai-compatible@2.0.56': + resolution: {integrity: sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 @@ -987,14 +987,14 @@ packages: peerDependencies: zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.30': - resolution: {integrity: sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==} + '@ai-sdk/provider-utils@4.0.35': + resolution: {integrity: sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider@3.0.10': - resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + '@ai-sdk/provider@3.0.13': + resolution: {integrity: sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw==} engines: {node: '>=18'} '@ai-sdk/provider@3.0.8': @@ -1012,8 +1012,8 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.106.0': - resolution: {integrity: sha512-ufwVvYNDBj2dzOGupBCTaNzBLxqcTnGOzI4z8Wouxlt+mT3J3HuOmatgCy1VmwCHOUueqZ41ERhm0O99OUcbWA==} + '@anthropic-ai/sdk@0.109.1': + resolution: {integrity: sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==} hasBin: true peerDependencies: zod: 3.25.76 @@ -1021,123 +1021,102 @@ packages: zod: optional: true - '@anthropic-ai/vertex-sdk@0.18.0': - resolution: {integrity: sha512-o79bEmNwfRyq8Pv1X4P7zXfvOPmNBnzzg3TtchyWqy0mpxtTYQoEMJUHCJXQYEYdHyZKnt3bIGxbPCNiBcDY7Q==} + '@anthropic-ai/vertex-sdk@0.19.0': + resolution: {integrity: sha512-Ja5NkDAmdCcvCJCvkY/6uJ+9krOiXFN56dzb+8n5apElHrYpUMlOCHCVRuLLsJCVWTsN8w60sgN9RSXZ+LPqNA==} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-bedrock-runtime@3.1075.0': - resolution: {integrity: sha512-LDGtNMOxnMz0dw9q+8z0f/X+Soj8OyiYg5zPcqToLh6H9/HHlazogFj7PXqFLOhnvhCqyAvKVAC1ZrL0RX418g==} + '@aws-sdk/client-bedrock-runtime@3.1078.0': + resolution: {integrity: sha512-GGIpsHOk+zMRQMgxd+5D7Kfhpe6qzyGP4shGzb7NwYqAEleCW9PgX5xXuVQEESkhGX5kqMkDPfT+gg6PN2gczg==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-cognito-identity@3.1075.0': - resolution: {integrity: sha512-typFcdyFwIPt86QPKsO7xwcsYoEmNcDpoNqn0tqa4+Lss7niNKQzPe/765XNgAHO3I1ixiT1OKv8KD6M+CKw2w==} + '@aws-sdk/client-cognito-identity@3.1078.0': + resolution: {integrity: sha512-BYy0X/+GMXlitKShxkdTsCexWwDrn8usY2Y2Z06M5MSi4aRT3Ce5ilyA6OubQUqOWfsmDMYrm8oBNaTIcQFyrg==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.974.23': - resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + '@aws-sdk/core@3.974.27': + resolution: {integrity: sha512-WRWEgIq6vx+NU6ot3VrRu4Jovj9MIObitSi6of/GV5THDDPccBhivCRNkWJutMM+m3GvdeI3l/UbGNcoOobxOA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-cognito-identity@3.972.48': - resolution: {integrity: sha512-dTSY4wCPx87Gd1peDcTop1li61f6KLAs3LSlq/omnCxpIOvMDpAQjylJ7ZDaZk6KA88+oZ0k/XuVSou1YuWI/w==} + '@aws-sdk/credential-provider-cognito-identity@3.972.52': + resolution: {integrity: sha512-m+akZFJsghShferf2xsMw0Hogl1jNIJl2zUoZBNTFyWvlaOj1aK5sMTzcnw8m1dICvlQ+lC4T1OPGGsmZ+ezXA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.49': - resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + '@aws-sdk/credential-provider-env@3.972.53': + resolution: {integrity: sha512-+KDA3uc/HZ1vIneGu5QMQb0gAXDYrm2vOE60+BJ7lS0YinMQ5i2oV4PR1A16XkF6K1IbSwjEHd1hQIIgMsK48w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.51': - resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + '@aws-sdk/credential-provider-http@3.972.55': + resolution: {integrity: sha512-1gBfkWY3RWeBlCoB9lIJjXMx45/54wxcgfzv6BY9otTmMrZPcNPi1v+MwZxxaCUg441NV3jsr1efnFNCXiW70g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.56': - resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + '@aws-sdk/credential-provider-ini@3.972.60': + resolution: {integrity: sha512-CV2md+PXvABwRjApWGhQ0wACy9WSFIhnUGrovLcjnjBCd/46TbuivLADtkF8IWNjtCQmQ+2IagSaxqBYqXBNAQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.55': - resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + '@aws-sdk/credential-provider-login@3.972.59': + resolution: {integrity: sha512-JG4S9yyA1GFzJdJXqLKrUzZbyK+VDp2QIsJD7YOicJHAhqymfHpDJIok2dLnhOdVB0I37RjdC53uOwCMVS00gw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.58': - resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + '@aws-sdk/credential-provider-node@3.972.62': + resolution: {integrity: sha512-S6Slq3Tx7bvFk5yc34XNADyZYTX2HUXvaFAnowGRQnhjBO8J/mP62Fn7lxvJwjaDyYm/7gh9h6HEHaltRyMFXw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.49': - resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + '@aws-sdk/credential-provider-process@3.972.53': + resolution: {integrity: sha512-EhfH+MQlqOMCkXIVa8MMObPzAQqwTTtxA7KhEJiyPeuNVA8PLOOUpgK7nBrgaDaGiIDLN/9LpGdaHuDjomeRTw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.55': - resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + '@aws-sdk/credential-provider-sso@3.972.59': + resolution: {integrity: sha512-h8793pOjcImx0SB+VcLONcaQQ57VAvKVuqyewQMRKqqH+CSXsG2dwOeLMUJPMxLdNvL7dXOM0ueTukyNUnu5mA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.55': - resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + '@aws-sdk/credential-provider-web-identity@3.972.59': + resolution: {integrity: sha512-VoyO9+vl3XVmpZwn4obskrWIkrA/Jf3lSe1E3ZERlaN9u0D4YZ6+HywC3+L98QOXqZesEfedk67gRER8tK8+8w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-providers@3.1075.0': - resolution: {integrity: sha512-2HoJ1IxwdzEryyUmZvl6dVKfgWBnS6NzSDl5hOqmbHns5Cy+wCQLDFU+HGVi+kTGZgSyMZIa0rH7fQvNf7v9jQ==} + '@aws-sdk/credential-providers@3.1078.0': + resolution: {integrity: sha512-V9Tr3MrNWUfTGgTMIr+WJaMC/VDbXY57BzrGDuyDZn7+vgZjAEG6nI5nMdfnTGdvV9wq1n0zZPYW2RDfcsWNCw==} engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.972.22': - resolution: {integrity: sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==} + '@aws-sdk/eventstream-handler-node@3.972.25': + resolution: {integrity: sha512-df7HN1ozwMrB9+59re9PM7tSLxLAcheMWc5u/KyfCPCAWtN/vP7y7RTUZOy48uT1K9MESisVeOPPzF3O1AW01A==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.972.18': - resolution: {integrity: sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==} + '@aws-sdk/middleware-eventstream@3.972.21': + resolution: {integrity: sha512-HvLgDnxBLaHi9E5K++6Vuk+1+qqn7Pmn8zrlzd+NXH3jBzwujnuzZtAR9WHPkbUGPO92FkoQWj/M1IsdxTlBmQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-websocket@3.972.31': - resolution: {integrity: sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==} + '@aws-sdk/middleware-websocket@3.972.35': + resolution: {integrity: sha512-7/ZAlq5o5A9FnRsQEftZvcct9LVZf1YaYmWR3eOzyJAdKU66YpOKy5ahUVvyBvCTLyO4Ej4yWIk8dllWNXPBsw==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.997.23': - resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + '@aws-sdk/nested-clients@3.997.27': + resolution: {integrity: sha512-A8PIePF9NIIOJ/4Lg1rl9xm/+QaKkHGetq+Z9wb5B+3Da31YYXRo8n7IDMh5C+HQI5eyEmjrwkGWVdYtnLtbXQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.35': - resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + '@aws-sdk/signature-v4-multi-region@3.996.38': + resolution: {integrity: sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1074.0': - resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} + '@aws-sdk/token-providers@3.1078.0': + resolution: {integrity: sha512-/uyXLBGu3Lw1GbBA2X66hcOMnKtMcqAIF+3/eHfxBQmUeXF2sdqozDPrTfEr/TnSd0D6deZar+eVyhEqqWu29w==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1075.0': - resolution: {integrity: sha512-SsunyegDXq68TaN5Iut8ElErGIAA6DeuKPKd5/v0lpSmZBI7ZKOC5OALyi1MRHsl/cuO/zHkJL3vKnNHdGaI+Q==} + '@aws-sdk/token-providers@3.1079.0': + resolution: {integrity: sha512-cbietrLlHPhhmbnMPTuDS4Zj/KNGhY+3vVhn6dwjO6Dqzrwothzg2srtcY34T9mlICsTXn34avDoWLHSntP54A==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.13': - resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + '@aws-sdk/types@3.973.15': + resolution: {integrity: sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.965.8': - resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + '@aws-sdk/xml-builder@3.972.33': + resolution: {integrity: sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.31': - resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.4': - resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} '@azu/format-text@1.0.2': @@ -2873,42 +2852,30 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@smithy/core@3.26.0': - resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + '@smithy/core@3.29.1': + resolution: {integrity: sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.4.2': - resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + '@smithy/credential-provider-imds@4.4.6': + resolution: {integrity: sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.5.2': - resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + '@smithy/fetch-http-handler@5.6.3': + resolution: {integrity: sha512-CwCc/7SMTj45y97MUnDTbTaxvtAsiNNRm81z3abROIuMbMsC2Iy5EKfkkVdsKrz8WExQAAMx1EJapq+9j4fFTQ==} engines: {node: '>=18.0.0'} - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/node-http-handler@4.8.2': - resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + '@smithy/node-http-handler@4.9.3': + resolution: {integrity: sha512-qZTa4gQFUo8RM02rk6q5UVTDLNrQ1oS20LsepBzqq1QBVc/EHJ03OOUADcqMZiXHArW+Y7+OGY0BpdTwZRq/Yg==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.5.2': - resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + '@smithy/signature-v4@5.6.2': + resolution: {integrity: sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g==} engines: {node: '>=18.0.0'} - '@smithy/types@4.15.0': - resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + '@smithy/types@4.15.1': + resolution: {integrity: sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==} engines: {node: '>=18.0.0'} - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -3574,8 +3541,8 @@ packages: peerDependencies: ai: '>=6.0.0' - ai@6.0.209: - resolution: {integrity: sha512-NE7uErmxOe+i77BPT1+AhW4wr7xzilbQLsKVmvGEyil5AjaF6C/m92hlPI4Xwtnv5n77D/29bizwcR27/S/gSQ==} + ai@6.0.218: + resolution: {integrity: sha512-HsyCUNaaYgX/b/kGOoYfKkqfT1HvpUKKDb8YkN1FKeCNZjKdqXLGY+cKBpYGIRAvsPuOHskxLxZ46cK1dTBWQQ==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 @@ -5004,18 +4971,10 @@ packages: fzf@0.5.2: resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - gaxios@7.1.5: resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} engines: {node: '>=18'} - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} @@ -5114,14 +5073,6 @@ packages: resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} engines: {node: '>=18'} - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} @@ -5140,10 +5091,6 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -6532,15 +6479,6 @@ packages: node-ensure@0.0.0: resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7576,15 +7514,14 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions - space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -7937,9 +7874,6 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -8259,11 +8193,6 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -8411,9 +8340,6 @@ packages: web-vitals@5.3.0: resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -8434,9 +8360,6 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -8657,17 +8580,17 @@ snapshots: '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/gateway@3.0.134(zod@3.25.76)': + '@ai-sdk/gateway@3.0.142(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@ai-sdk/provider': 3.0.13 + '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) '@vercel/oidc': 3.2.0 zod: 3.25.76 - '@ai-sdk/openai-compatible@2.0.51(zod@3.25.76)': + '@ai-sdk/openai-compatible@2.0.56(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@ai-sdk/provider': 3.0.13 + '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) zod: 3.25.76 '@ai-sdk/openai@3.0.27(zod@3.25.76)': @@ -8683,14 +8606,14 @@ snapshots: eventsource-parser: 3.1.0 zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.30(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.35(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider': 3.0.13 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 zod: 3.25.76 - '@ai-sdk/provider@3.0.10': + '@ai-sdk/provider@3.0.13': dependencies: json-schema: 0.4.0 @@ -8713,19 +8636,18 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.2.4 - '@anthropic-ai/sdk@0.106.0(zod@3.25.76)': + '@anthropic-ai/sdk@0.109.1(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 optionalDependencies: zod: 3.25.76 - '@anthropic-ai/vertex-sdk@0.18.0(zod@3.25.76)': + '@anthropic-ai/vertex-sdk@0.19.0(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.106.0(zod@3.25.76) - google-auth-library: 9.15.1 + '@anthropic-ai/sdk': 0.109.1(zod@3.25.76) + google-auth-library: 10.9.0 transitivePeerDependencies: - - encoding - supports-color - zod @@ -8737,268 +8659,226 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.13 - tslib: 2.8.1 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.13 - '@aws-sdk/util-locate-window': 3.965.8 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.13 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: + '@aws-sdk/client-bedrock-runtime@3.1078.0': + dependencies: + '@aws-sdk/core': 3.974.27 + '@aws-sdk/credential-provider-node': 3.972.62 + '@aws-sdk/eventstream-handler-node': 3.972.25 + '@aws-sdk/middleware-eventstream': 3.972.21 + '@aws-sdk/middleware-websocket': 3.972.35 + '@aws-sdk/token-providers': 3.1078.0 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/fetch-http-handler': 5.6.3 + '@smithy/node-http-handler': 4.9.3 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-crypto/util@5.2.0': + '@aws-sdk/client-cognito-identity@3.1078.0': dependencies: - '@aws-sdk/types': 3.973.13 - '@smithy/util-utf8': 2.3.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/credential-provider-node': 3.972.62 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/fetch-http-handler': 5.6.3 + '@smithy/node-http-handler': 4.9.3 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.1075.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.23 - '@aws-sdk/credential-provider-node': 3.972.58 - '@aws-sdk/eventstream-handler-node': 3.972.22 - '@aws-sdk/middleware-eventstream': 3.972.18 - '@aws-sdk/middleware-websocket': 3.972.31 - '@aws-sdk/token-providers': 3.1075.0 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/fetch-http-handler': 5.5.2 - '@smithy/node-http-handler': 4.8.2 - '@smithy/types': 4.15.0 - tslib: 2.8.1 - - '@aws-sdk/client-cognito-identity@3.1075.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.23 - '@aws-sdk/credential-provider-node': 3.972.58 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/fetch-http-handler': 5.5.2 - '@smithy/node-http-handler': 4.8.2 - '@smithy/types': 4.15.0 - tslib: 2.8.1 - - '@aws-sdk/core@3.974.23': + '@aws-sdk/core@3.974.27': dependencies: - '@aws-sdk/types': 3.973.13 - '@aws-sdk/xml-builder': 3.972.31 - '@aws/lambda-invoke-store': 0.2.4 - '@smithy/core': 3.26.0 - '@smithy/signature-v4': 5.5.2 - '@smithy/types': 4.15.0 + '@aws-sdk/types': 3.973.15 + '@aws-sdk/xml-builder': 3.972.33 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.29.1 + '@smithy/signature-v4': 5.6.2 + '@smithy/types': 4.15.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-cognito-identity@3.972.48': + '@aws-sdk/credential-provider-cognito-identity@3.972.52': dependencies: - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.49': + '@aws-sdk/credential-provider-env@3.972.53': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.51': + '@aws-sdk/credential-provider-http@3.972.55': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/fetch-http-handler': 5.5.2 - '@smithy/node-http-handler': 4.8.2 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/fetch-http-handler': 5.6.3 + '@smithy/node-http-handler': 4.9.3 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.56': - dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/credential-provider-env': 3.972.49 - '@aws-sdk/credential-provider-http': 3.972.51 - '@aws-sdk/credential-provider-login': 3.972.55 - '@aws-sdk/credential-provider-process': 3.972.49 - '@aws-sdk/credential-provider-sso': 3.972.55 - '@aws-sdk/credential-provider-web-identity': 3.972.55 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/credential-provider-imds': 4.4.2 - '@smithy/types': 4.15.0 + '@aws-sdk/credential-provider-ini@3.972.60': + dependencies: + '@aws-sdk/core': 3.974.27 + '@aws-sdk/credential-provider-env': 3.972.53 + '@aws-sdk/credential-provider-http': 3.972.55 + '@aws-sdk/credential-provider-login': 3.972.59 + '@aws-sdk/credential-provider-process': 3.972.53 + '@aws-sdk/credential-provider-sso': 3.972.59 + '@aws-sdk/credential-provider-web-identity': 3.972.59 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/credential-provider-imds': 4.4.6 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.55': + '@aws-sdk/credential-provider-login@3.972.59': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.58': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.49 - '@aws-sdk/credential-provider-http': 3.972.51 - '@aws-sdk/credential-provider-ini': 3.972.56 - '@aws-sdk/credential-provider-process': 3.972.49 - '@aws-sdk/credential-provider-sso': 3.972.55 - '@aws-sdk/credential-provider-web-identity': 3.972.55 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/credential-provider-imds': 4.4.2 - '@smithy/types': 4.15.0 + '@aws-sdk/credential-provider-node@3.972.62': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.53 + '@aws-sdk/credential-provider-http': 3.972.55 + '@aws-sdk/credential-provider-ini': 3.972.60 + '@aws-sdk/credential-provider-process': 3.972.53 + '@aws-sdk/credential-provider-sso': 3.972.59 + '@aws-sdk/credential-provider-web-identity': 3.972.59 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/credential-provider-imds': 4.4.6 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.49': + '@aws-sdk/credential-provider-process@3.972.53': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.55': + '@aws-sdk/credential-provider-sso@3.972.59': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/token-providers': 3.1074.0 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/token-providers': 3.1079.0 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.55': + '@aws-sdk/credential-provider-web-identity@3.972.59': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/credential-providers@3.1075.0': - dependencies: - '@aws-sdk/client-cognito-identity': 3.1075.0 - '@aws-sdk/core': 3.974.23 - '@aws-sdk/credential-provider-cognito-identity': 3.972.48 - '@aws-sdk/credential-provider-env': 3.972.49 - '@aws-sdk/credential-provider-http': 3.972.51 - '@aws-sdk/credential-provider-ini': 3.972.56 - '@aws-sdk/credential-provider-login': 3.972.55 - '@aws-sdk/credential-provider-node': 3.972.58 - '@aws-sdk/credential-provider-process': 3.972.49 - '@aws-sdk/credential-provider-sso': 3.972.55 - '@aws-sdk/credential-provider-web-identity': 3.972.55 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/credential-provider-imds': 4.4.2 - '@smithy/types': 4.15.0 + '@aws-sdk/credential-providers@3.1078.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1078.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/credential-provider-cognito-identity': 3.972.52 + '@aws-sdk/credential-provider-env': 3.972.53 + '@aws-sdk/credential-provider-http': 3.972.55 + '@aws-sdk/credential-provider-ini': 3.972.60 + '@aws-sdk/credential-provider-login': 3.972.59 + '@aws-sdk/credential-provider-node': 3.972.62 + '@aws-sdk/credential-provider-process': 3.972.53 + '@aws-sdk/credential-provider-sso': 3.972.59 + '@aws-sdk/credential-provider-web-identity': 3.972.59 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/credential-provider-imds': 4.4.6 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/eventstream-handler-node@3.972.22': + '@aws-sdk/eventstream-handler-node@3.972.25': dependencies: - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.972.18': + '@aws-sdk/middleware-eventstream@3.972.21': dependencies: - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.972.31': + '@aws-sdk/middleware-websocket@3.972.35': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/fetch-http-handler': 5.5.2 - '@smithy/signature-v4': 5.5.2 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/fetch-http-handler': 5.6.3 + '@smithy/signature-v4': 5.6.2 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.23': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.23 - '@aws-sdk/signature-v4-multi-region': 3.996.35 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/fetch-http-handler': 5.5.2 - '@smithy/node-http-handler': 4.8.2 - '@smithy/types': 4.15.0 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.996.35': + '@aws-sdk/nested-clients@3.997.27': dependencies: - '@aws-sdk/types': 3.973.13 - '@smithy/signature-v4': 5.5.2 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/signature-v4-multi-region': 3.996.38 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/fetch-http-handler': 5.6.3 + '@smithy/node-http-handler': 4.9.3 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1074.0': + '@aws-sdk/signature-v4-multi-region@3.996.38': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/types': 3.973.15 + '@smithy/signature-v4': 5.6.2 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1075.0': + '@aws-sdk/token-providers@3.1078.0': dependencies: - '@aws-sdk/core': 3.974.23 - '@aws-sdk/nested-clients': 3.997.23 - '@aws-sdk/types': 3.973.13 - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/types@3.973.13': + '@aws-sdk/token-providers@3.1079.0': dependencies: - '@smithy/types': 4.15.0 + '@aws-sdk/core': 3.974.27 + '@aws-sdk/nested-clients': 3.997.27 + '@aws-sdk/types': 3.973.15 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.965.8': + '@aws-sdk/types@3.973.15': dependencies: + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.31': + '@aws-sdk/xml-builder@3.972.33': dependencies: - '@smithy/types': 4.15.0 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.4': {} + '@aws/lambda-invoke-store@0.3.0': {} '@azu/format-text@1.0.2': {} @@ -10780,52 +10660,37 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@smithy/core@3.26.0': + '@smithy/core@3.29.1': dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.15.0 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.4.2': + '@smithy/credential-provider-imds@4.4.6': dependencies: - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.5.2': + '@smithy/fetch-http-handler@5.6.3': dependencies: - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@smithy/is-array-buffer@2.2.0': + '@smithy/node-http-handler@4.9.3': dependencies: + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.8.2': + '@smithy/signature-v4@5.6.2': dependencies: - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 tslib: 2.8.1 - '@smithy/signature-v4@5.5.2': + '@smithy/types@4.15.1': dependencies: - '@smithy/core': 3.26.0 - '@smithy/types': 4.15.0 - tslib: 2.8.1 - - '@smithy/types@4.15.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 '@stablelib/base64@1.0.1': {} @@ -11585,22 +11450,22 @@ snapshots: agent-base@7.1.3: {} - ai-sdk-provider-poe@2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.18(ai@6.0.218(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) - '@ai-sdk/openai-compatible': 2.0.51(zod@3.25.76) + '@ai-sdk/openai-compatible': 2.0.56(zod@3.25.76) '@ai-sdk/provider': 3.0.8 '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - ai: 6.0.209(zod@3.25.76) + ai: 6.0.218(zod@3.25.76) transitivePeerDependencies: - zod - ai@6.0.209(zod@3.25.76): + ai@6.0.218(zod@3.25.76): dependencies: - '@ai-sdk/gateway': 3.0.134(zod@3.25.76) - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@ai-sdk/gateway': 3.0.142(zod@3.25.76) + '@ai-sdk/provider': 3.0.13 + '@ai-sdk/provider-utils': 4.0.35(zod@3.25.76) '@opentelemetry/api': 1.9.0 zod: 3.25.76 @@ -13227,17 +13092,6 @@ snapshots: fzf@0.5.2: {} - gaxios@6.7.1: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gaxios@7.1.5: dependencies: extend: 3.0.2 @@ -13246,15 +13100,6 @@ snapshots: transitivePeerDependencies: - supports-color - gcp-metadata@6.1.1: - dependencies: - gaxios: 6.7.1 - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - gcp-metadata@8.1.2: dependencies: gaxios: 7.1.5 @@ -13375,20 +13220,6 @@ snapshots: transitivePeerDependencies: - supports-color - google-auth-library@9.15.1: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1 - gcp-metadata: 6.1.1 - gtoken: 7.1.0 - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - google-logging-utils@0.0.2: {} - google-logging-utils@1.1.3: {} gopd@1.2.0: {} @@ -13404,14 +13235,6 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - gtoken@7.1.0: - dependencies: - gaxios: 6.7.1 - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - hachure-fill@0.5.2: {} has-bigints@1.1.0: {} @@ -15075,10 +14898,6 @@ snapshots: node-ensure@0.0.0: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -16328,11 +16147,9 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.6: {} + source-map@0.7.4: {} - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 + source-map@0.7.6: {} space-separated-tokens@1.1.5: {} @@ -16720,8 +16537,6 @@ snapshots: dependencies: tldts: 6.1.86 - tr46@0.0.3: {} - tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -17062,8 +16877,6 @@ snapshots: uuid@8.3.2: {} - uuid@9.0.1: {} - validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -17171,8 +16984,6 @@ snapshots: web-vitals@5.3.0: {} - webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: @@ -17188,11 +16999,6 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/src/package.json b/src/package.json index 181f30122e..4ed4e9df07 100644 --- a/src/package.json +++ b/src/package.json @@ -457,8 +457,8 @@ "clean": "rimraf README.md CHANGELOG.md LICENSE dist logs mock .turbo" }, "dependencies": { - "@anthropic-ai/sdk": "^0.106.0", - "@anthropic-ai/vertex-sdk": "^0.18.0", + "@anthropic-ai/sdk": "^0.109.0", + "@anthropic-ai/vertex-sdk": "^0.19.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", "@aws-sdk/credential-providers": "^3.922.0", "@google/genai": "^1.29.1", @@ -484,7 +484,7 @@ "fzf": "^0.5.2", "get-folder-size": "^5.0.0", "global-agent": "^3.0.0", - "google-auth-library": "^9.15.1", + "google-auth-library": "^10.2.0", "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", @@ -524,7 +524,7 @@ "zod": "3.25.76" }, "devDependencies": { - "@ai-sdk/openai-compatible": "2.0.51", + "@ai-sdk/openai-compatible": "2.0.56", "@roo-code/build": "workspace:^", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", @@ -540,7 +540,7 @@ "@types/vscode": "1.100.0", "@vitest/coverage-v8": "4.1.9", "@vscode/vsce": "3.9.2", - "ai": "6.0.209", + "ai": "6.0.218", "esbuild-wasm": "0.25.12", "execa": "9.6.1", "mkdirp": "3.0.1", From 50801daafcc8ca2202839df611a8c4a53434455c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:27:05 +0000 Subject: [PATCH 17/89] fix(deps): update frontend and ui stack (#818) * fix(deps): update frontend and ui stack * refactor(webview-ui): replace deprecated lucide-react icon aliases --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- pnpm-lock.yaml | 1326 +++++++---------- src/package.json | 10 +- webview-ui/package.json | 6 +- webview-ui/src/components/chat/ChatRow.tsx | 4 +- .../src/components/chat/CodeIndexPopover.tsx | 4 +- .../components/settings/ApiConfigManager.tsx | 4 +- .../src/components/settings/SettingsView.tsx | 8 +- 7 files changed, 520 insertions(+), 842 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 081ac8817e..cd950c2178 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,7 @@ importers: dependencies: '@inkjs/ui': specifier: ^2.0.0 - version: 2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.3)) + version: 2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.7)) '@roo-code/core': specifier: workspace:^ version: link:../../packages/core @@ -105,19 +105,19 @@ importers: version: 3.1.0 ink: specifier: ^6.6.0 - version: 6.6.0(@types/react@18.3.31)(react@19.2.3) + version: 6.6.0(@types/react@18.3.31)(react@19.2.7) p-wait-for: specifier: ^5.0.2 version: 5.0.2 react: specifier: ^19.1.0 - version: 19.2.3 + version: 19.2.7 superjson: specifier: ^2.2.6 version: 2.2.6 zustand: specifier: ^5.0.0 - version: 5.0.9(@types/react@18.3.31)(react@19.2.3) + version: 5.0.9(@types/react@18.3.31)(react@19.2.7) devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -470,7 +470,7 @@ importers: specifier: ^0.5.0 version: 0.5.0 axios: - specifier: ^1.12.0 + specifier: ^1.18.1 version: 1.18.1 chokidar: specifier: ^4.0.1 @@ -518,7 +518,7 @@ importers: specifier: ^5.0.7 version: 5.0.7 json-stream-stringify: - specifier: ^3.1.6 + specifier: ^3.1.7 version: 3.1.7 lodash.debounce: specifier: ^4.0.8 @@ -563,13 +563,13 @@ importers: specifier: ^1.2.0 version: 1.2.0 reconnecting-eventsource: - specifier: ^1.6.4 + specifier: ^1.6.5 version: 1.6.5 safe-stable-stringify: specifier: ^2.5.0 version: 2.5.0 sanitize-filename: - specifier: ^1.6.3 + specifier: ^1.6.4 version: 1.6.4 say: specifier: ^0.16.0 @@ -593,7 +593,7 @@ importers: specifier: ^1.0.21 version: 1.0.22 tree-sitter-wasms: - specifier: ^0.1.12 + specifier: ^0.1.13 version: 0.1.13 undici: specifier: 6.27.0 @@ -694,55 +694,55 @@ importers: dependencies: '@radix-ui/react-alert-dialog': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.5 - version: 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': specifier: ^1.1.3 - version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.5 - version: 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.19(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': specifier: ^1.1.5 - version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-progress': specifier: ^1.1.2 - version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.6 - version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.2 - version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slider': specifier: ^1.2.3 - version: 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.2 - version: 1.2.3(@types/react@18.3.31)(react@18.3.1) + version: 1.3.0(@types/react@18.3.31)(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.8 - version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@roo-code/types': specifier: workspace:^ version: link:../packages/types '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.6(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.3.2(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@tanstack/react-query': specifier: ^5.68.0 version: 5.76.1(react@18.3.1) @@ -756,7 +756,7 @@ importers: specifier: ^0.7.2 version: 0.7.2 axios: - specifier: ^1.12.0 + specifier: ^1.18.1 version: 1.18.1 class-variance-authority: specifier: ^0.7.1 @@ -795,8 +795,8 @@ importers: specifier: ^11.1.0 version: 11.2.2 lucide-react: - specifier: ^0.518.0 - version: 0.518.0(react@18.3.1) + specifier: ^0.577.0 + version: 0.577.0(react@18.3.1) mermaid: specifier: ^11.4.1 version: 11.16.0 @@ -846,7 +846,7 @@ importers: specifier: ^6.0.0 version: 6.0.0 remove-markdown: - specifier: ^0.6.0 + specifier: ^0.6.4 version: 0.6.4 shell-quote: specifier: ^1.8.2 @@ -862,16 +862,16 @@ importers: version: 2.0.2 styled-components: specifier: ^6.1.13 - version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 6.4.3(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^3.0.0 - version: 3.3.0 + version: 3.6.0 tailwindcss: specifier: ^4.0.0 - version: 4.1.6 + version: 4.3.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@4.1.6) + version: 1.0.7(tailwindcss@4.3.2) unist-util-visit: specifier: ^5.0.0 version: 5.0.0 @@ -1005,10 +1005,6 @@ packages: resolution: {integrity: sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==} engines: {node: '>=18'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -1387,14 +1383,11 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emotion/is-prop-valid@1.2.2': - resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} - - '@emotion/memoize@0.8.1': - resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} - '@emotion/unitless@0.8.1': - resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -1600,20 +1593,20 @@ packages: '@fast-csv/parse@4.3.6': resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} - '@floating-ui/core@1.7.0': - resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.0': - resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/react-dom@2.1.2': - resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.9': - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@google/genai@1.52.0': resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} @@ -1675,10 +1668,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@jest/expect-utils@29.7.0': resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2043,30 +2032,14 @@ packages: resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==} engines: {node: '>=18.0.0', pnpm: '>=8'} - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} - '@radix-ui/primitive@1.1.2': - resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} - - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-alert-dialog@1.1.13': - resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==} - peerDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - '@radix-ui/react-arrow@1.1.6': - resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==} + '@radix-ui/react-alert-dialog@1.1.18': + resolution: {integrity: sha512-6c2cXpNlAgHDhKguK24XcWHHayMpK+lk7/WwBXBco+ZJ4Dv7xP++GBM280KgTD/HCRu3jSdfe8WQiZssonYaIA==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2078,8 +2051,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-checkbox@1.3.1': - resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==} + '@radix-ui/react-arrow@1.1.11': + resolution: {integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2091,8 +2064,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collapsible@1.1.10': - resolution: {integrity: sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==} + '@radix-ui/react-checkbox@1.3.6': + resolution: {integrity: sha512-eUEUoGMDpfkgHWSE97ZZaUJtzR1M7EKnNIpD1Q16+8JR9NWghcaqMulx9PuCQ720w0UclfYn6FEbCdd5Hx087g==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2104,8 +2077,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.6': - resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==} + '@radix-ui/react-collapsible@1.1.15': + resolution: {integrity: sha512-8A1zibu5skAQ+UVbaeNH5hVMibiFCRJzgMuM14LTWGttnTZKQL9jwYnhAbHRuxrtCqPXa4JvvnVUq1pTNgyZYw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2117,8 +2090,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + '@radix-ui/react-collection@1.1.11': + resolution: {integrity: sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2130,8 +2103,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2139,8 +2112,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2148,8 +2121,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.13': - resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==} + '@radix-ui/react-dialog@1.1.18': + resolution: {integrity: sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2161,8 +2134,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2170,8 +2143,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.9': - resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} + '@radix-ui/react-dismissable-layer@1.1.14': + resolution: {integrity: sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2183,8 +2156,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.14': - resolution: {integrity: sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==} + '@radix-ui/react-dropdown-menu@2.1.19': + resolution: {integrity: sha512-HZccBkbK0LOi8nYKIp5jll/zIRW0cCOmG6WWyqsSpmXCU+ZlcBbTqIwlBvPCu886C5RVu6c/kHV7xSP8IgYNHw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2196,8 +2169,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.2': - resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2205,8 +2178,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.6': - resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==} + '@radix-ui/react-focus-scope@1.1.11': + resolution: {integrity: sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2223,56 +2196,17 @@ packages: peerDependencies: react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} - peerDependencies: - '@types/react': 18.3.31 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-menu@2.1.14': - resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==} - peerDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popover@1.1.13': - resolution: {integrity: sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==} - peerDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.2.6': - resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==} + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} peerDependencies: '@types/react': 18.3.31 - '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-portal@1.1.8': - resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} + '@radix-ui/react-menu@2.1.19': + resolution: {integrity: sha512-Mht9BVd1AIsNFVQr4KG3bIK7XQn5IXF0TL/2ObsrzOdc1loaly/+kBDL5roSCYn8j8XZkvpOD0WYLz2FQtH1Eg==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2284,8 +2218,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.4': - resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + '@radix-ui/react-popover@1.1.18': + resolution: {integrity: sha512-qdXDes+eHlnMUGlBAAAe5EG7oOQvqsXuq4mq585diMudg80iB+jHbsSeG3+Q4eWNsogNyhqU2p/3i+Y0iEepqg==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2297,8 +2231,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + '@radix-ui/react-popper@1.3.2': + resolution: {integrity: sha512-3QXNeMkdshed1MR3LNoiCirBywRFPkD8ETJa/HlPuLwSajaQixf2ro+isoDNJlGABg9ug41XuZpINZJIle4XWg==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2310,8 +2244,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.2': - resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + '@radix-ui/react-portal@1.1.13': + resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2323,8 +2257,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2336,8 +2270,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-progress@1.1.6': - resolution: {integrity: sha512-QzN9a36nKk2eZKMf9EBCia35x3TT+SOgZuzQBVIHyRrmYYi73VYBRK3zKwdJ6az/F5IZ6QlacGJBg7zfB85liA==} + '@radix-ui/react-primitive@2.1.7': + resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2349,8 +2283,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.3.8': - resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + '@radix-ui/react-progress@1.1.11': + resolution: {integrity: sha512-KqiGJcFaZDc+BvveAgU3ZhACg2MvSUDrCBx4lRR/ZVRNal0bvt8lBpvnSkep9heeOuF8Qfw3fszLDX4OpQ2NVw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2362,8 +2296,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + '@radix-ui/react-radio-group@1.4.2': + resolution: {integrity: sha512-W8Uo9riHnlzLLWy+r2mVHUyuEWqD/+be4PZzbEvaGoFSBDHkm+GYWjtcE6u3AmPKNyfanWpnVfpZ2GqPCdzzsw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2375,8 +2309,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.9': - resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} + '@radix-ui/react-roving-focus@1.1.14': + resolution: {integrity: sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2388,8 +2322,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.2.4': - resolution: {integrity: sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==} + '@radix-ui/react-select@2.3.2': + resolution: {integrity: sha512-brXD6C/V0fVK0DDbscLVw6LsXrjQ+ay8jdOBaN+tLb4vsHsAMm6Gt6eT77wHX1Eq8GPtD5rJ+RxFtfDozsb4+Q==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2401,8 +2335,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.6': - resolution: {integrity: sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==} + '@radix-ui/react-separator@1.1.11': + resolution: {integrity: sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2414,8 +2348,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.3.4': - resolution: {integrity: sha512-Cp6hEmQtRJFci285vkdIJ+HCDLTRDk+25VhFwa1fcubywjMUE3PynBgtN5RLudOgSCYMlT4jizCXdmV+8J7Y2w==} + '@radix-ui/react-slider@1.4.2': + resolution: {integrity: sha512-qt5C1ppJz66aUDrH1VccjPrq7aFchK0wBrn6xsxlCHNUyE57dRRQ7lp1QFpF7OscMexZF8MCGBTVBlENHPkNiA==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2427,17 +2361,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.2': - resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} - peerDependencies: - '@types/react': 18.3.31 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + '@radix-ui/react-slot@1.3.0': + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2445,8 +2370,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-tooltip@1.2.6': - resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==} + '@radix-ui/react-tooltip@1.2.11': + resolution: {integrity: sha512-8XZ6Py3y3W2nEzAUGCN5cfVKaUi+CVApcz1d6lrNVVf2hvYEixMRkq8k9ggPKnQUpRRuOV5avt8uvxViH2jLwA==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2458,8 +2383,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2467,8 +2392,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2476,8 +2401,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2485,8 +2410,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2494,8 +2419,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2503,8 +2428,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2512,8 +2437,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} peerDependencies: '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2521,17 +2446,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': 18.3.31 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.2': - resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==} + '@radix-ui/react-visually-hidden@1.2.7': + resolution: {integrity: sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==} peerDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7 @@ -2543,8 +2459,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} '@rolldown/binding-android-arm64@1.1.3': resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} @@ -2882,65 +2798,65 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tailwindcss/node@4.1.6': - resolution: {integrity: sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==} + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} - '@tailwindcss/oxide-android-arm64@4.1.6': - resolution: {integrity: sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.6': - resolution: {integrity: sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.6': - resolution: {integrity: sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.6': - resolution: {integrity: sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': - resolution: {integrity: sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': - resolution: {integrity: sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.6': - resolution: {integrity: sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.6': - resolution: {integrity: sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.6': - resolution: {integrity: sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.6': - resolution: {integrity: sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==} + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2951,24 +2867,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': - resolution: {integrity: sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.6': - resolution: {integrity: sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.6': - resolution: {integrity: sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + engines: {node: '>= 20'} - '@tailwindcss/vite@4.1.6': - resolution: {integrity: sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==} + '@tailwindcss/vite@4.3.2': + resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} peerDependencies: vite: 8.1.0 @@ -3303,9 +3219,6 @@ packages: resolution: {integrity: sha512-B6JnMic4NAZ4mLWmRi4RvayCN2HZQvpcVF0MkoqubtuZx1AQB0/kRlrngGiocEPyO7R+TFocTEoLKQ0HzmEOPw==} deprecated: This is a stub types definition. stacktrace-js provides its own type definitions, so you do not need this installed. - '@types/stylis@4.2.5': - resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3615,8 +3528,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} aria-query@5.3.0: @@ -3925,10 +3838,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -4533,8 +4442,8 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -5814,130 +5723,66 @@ packages: cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.29.2: - resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.29.2: - resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.29.2: - resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.29.2: - resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.29.2: - resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.29.2: - resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.29.2: - resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.29.2: - resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.29.2: - resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.29.2: - resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.29.2: - resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} - engines: {node: '>= 12.0.0'} - lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} @@ -6088,8 +5933,8 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.518.0: - resolution: {integrity: sha512-kFg34uQqnVl/7HwAiigxPSpj//43VIVHQbMygQPtS1yT4btMXHCWUipHcgcXHD2pm1Z2nUBA/M+Vnh/YmWXQUw==} + lucide-react@0.577.0: + resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -6383,10 +6228,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -6855,10 +6696,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -7062,8 +6899,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.6.3: - resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} engines: {node: '>=10'} peerDependencies: '@types/react': 18.3.31 @@ -7110,8 +6947,8 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} read-pkg@9.0.1: @@ -7417,9 +7254,6 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -7710,15 +7544,24 @@ packages: style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} - styled-components@6.1.18: - resolution: {integrity: sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==} + styled-components@6.4.3: + resolution: {integrity: sha512-wYXrhu+JmDjZ1Tv7O0OopGTfztbzun43Pjjhh2H+xc0h5A09dwpZ5FJbrifJDcL8g5TA9btpWOX2+iSRuJTExw==} engines: {node: '>= 16'} peerDependencies: + css-to-react-native: '>= 3.2.0' react: '>= 16.8.0' react-dom: '>= 16.8.0' + react-native: '>= 0.68.0' + peerDependenciesMeta: + css-to-react-native: + optional: true + react-dom: + optional: true + react-native: + optional: true - stylis@4.3.2: - resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} @@ -7758,19 +7601,19 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwind-merge@3.3.0: - resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@4.1.6: - resolution: {integrity: sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==} + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} tar-fs@3.1.1: @@ -7783,11 +7626,6 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -7922,9 +7760,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -8469,10 +8304,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -8626,11 +8457,6 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 @@ -9300,13 +9126,11 @@ snapshots: tslib: 2.8.1 optional: true - '@emotion/is-prop-valid@1.2.2': + '@emotion/is-prop-valid@1.4.0': dependencies: - '@emotion/memoize': 0.8.1 + '@emotion/memoize': 0.9.0 - '@emotion/memoize@0.8.1': {} - - '@emotion/unitless@0.8.1': {} + '@emotion/memoize@0.9.0': {} '@esbuild/aix-ppc64@0.28.1': optional: true @@ -9453,22 +9277,22 @@ snapshots: lodash.isundefined: 3.0.1 lodash.uniq: 4.5.0 - '@floating-ui/core@1.7.0': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.9 + '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.0': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.0 - '@floating-ui/utils': 0.2.9 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.0 + '@floating-ui/dom': 1.7.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/utils@0.2.9': {} + '@floating-ui/utils@0.2.11': {} '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))': dependencies: @@ -9508,13 +9332,13 @@ snapshots: '@iconify/types': 2.0.0 import-meta-resolve: 4.2.0 - '@inkjs/ui@2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.3))': + '@inkjs/ui@2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.7))': dependencies: chalk: 5.6.2 cli-spinners: 3.3.0 deepmerge: 4.3.1 figures: 6.1.0 - ink: 6.6.0(@types/react@18.3.31)(react@19.2.3) + ink: 6.6.0(@types/react@18.3.31)(react@19.2.7) '@inquirer/external-editor@1.0.2(@types/node@20.19.43)': dependencies: @@ -9532,10 +9356,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@jest/expect-utils@29.7.0': dependencies: jest-get-type: 29.6.3 @@ -9897,170 +9717,155 @@ snapshots: '@qdrant/openapi-typescript-fetch@1.2.6': {} - '@radix-ui/number@1.1.1': {} + '@radix-ui/number@1.1.2': {} - '@radix-ui/primitive@1.1.2': {} - - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7(@types/react@18.3.31) + '@radix-ui/primitive@1.1.4': {} - '@radix-ui/react-arrow@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-checkbox@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collapsible@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collection@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.3(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-context@1.1.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-context@1.1.4(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - aria-hidden: 1.2.4 + '@radix-ui/react-dialog@1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-direction@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-direction@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-dropdown-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.19(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-menu': 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-menu': 2.1.19(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.4(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: @@ -10071,345 +9876,295 @@ snapshots: dependencies: react: 18.3.1 - '@radix-ui/react-id@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-id@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.31 - - '@radix-ui/react-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - aria-hidden: 1.2.4 + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7(@types/react@18.3.31) - - '@radix-ui/react-popover@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 - '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-popper@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/rect': 1.1.1 + '@radix-ui/react-menu@2.1.19(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-portal@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popover@1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/rect': 1.1.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-progress@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-progress@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.31 - '@types/react-dom': 18.3.7(@types/react@18.3.31) - - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-radio-group@1.4.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-roving-focus@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-select@2.2.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - aria-hidden: 1.2.4 + '@radix-ui/react-select@2.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-separator@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-separator@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-slider@1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-slider@1.4.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-slot@1.2.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-slot@1.3.0(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-slot@1.2.3(@types/react@18.3.31)(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.31 - - '@radix-ui/react-tooltip@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip@1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.31)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.31 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.3(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/rect': 1.1.1 + '@radix-ui/rect': 1.1.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.31)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: '@types/react': 18.3.31 - '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/rect@1.1.1': {} + '@radix-ui/rect@1.1.2': {} '@rolldown/binding-android-arm64@1.1.3': optional: true @@ -10697,75 +10452,72 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tailwindcss/node@4.1.6': + '@tailwindcss/node@4.3.2': dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 jiti: 2.7.0 - lightningcss: 1.29.2 + lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.6 + tailwindcss: 4.3.2 - '@tailwindcss/oxide-android-arm64@4.1.6': + '@tailwindcss/oxide-android-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.6': + '@tailwindcss/oxide-darwin-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.6': + '@tailwindcss/oxide-darwin-x64@4.3.2': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.6': + '@tailwindcss/oxide-freebsd-x64@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.6': + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.6': + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.6': + '@tailwindcss/oxide-linux-x64-musl@4.3.2': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.6': + '@tailwindcss/oxide-wasm32-wasi@4.3.2': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.6': + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': optional: true - '@tailwindcss/oxide@4.1.6': - dependencies: - detect-libc: 2.1.2 - tar: 7.4.3 + '@tailwindcss/oxide@4.3.2': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.6 - '@tailwindcss/oxide-darwin-arm64': 4.1.6 - '@tailwindcss/oxide-darwin-x64': 4.1.6 - '@tailwindcss/oxide-freebsd-x64': 4.1.6 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.6 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.6 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.6 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.6 - '@tailwindcss/oxide-linux-x64-musl': 4.1.6 - '@tailwindcss/oxide-wasm32-wasi': 4.1.6 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.6 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.6 - - '@tailwindcss/vite@4.1.6(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': - dependencies: - '@tailwindcss/node': 4.1.6 - '@tailwindcss/oxide': 4.1.6 - tailwindcss: 4.1.6 + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + + '@tailwindcss/vite@4.3.2(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 + tailwindcss: 4.3.2 vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@tanstack/query-core@5.76.0': {} @@ -11132,8 +10884,6 @@ snapshots: dependencies: stacktrace-js: 2.0.2 - '@types/stylis@4.2.5': {} - '@types/trusted-types@2.0.7': optional: true @@ -11553,7 +11303,7 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.4: + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -11823,7 +11573,8 @@ snapshots: camelcase@6.3.0: {} - camelize@1.0.1: {} + camelize@1.0.1: + optional: true caniuse-lite@1.0.30001766: {} @@ -11885,8 +11636,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@3.0.0: {} - ci-info@2.0.0: {} ci-info@3.9.0: {} @@ -11938,10 +11687,10 @@ snapshots: cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -12059,7 +11808,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-color-keywords@1.0.0: {} + css-color-keywords@1.0.0: + optional: true css-in-js-utils@3.1.0: dependencies: @@ -12078,6 +11828,7 @@ snapshots: camelize: 1.0.1 css-color-keywords: 1.0.0 postcss-value-parser: 4.2.0 + optional: true css-tree@1.1.3: dependencies: @@ -12481,10 +12232,10 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.1: + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.3.3 enquirer@2.4.1: dependencies: @@ -13485,7 +13236,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.31 - ink@6.6.0(@types/react@18.3.31)(react@19.2.3): + ink@6.6.0(@types/react@18.3.31)(react@19.2.7): dependencies: '@alcalzone/ansi-tokenize': 0.2.3 ansi-escapes: 7.3.0 @@ -13500,8 +13251,8 @@ snapshots: indent-string: 5.0.0 is-in-ci: 2.0.0 patch-console: 2.0.0 - react: 19.2.3 - react-reconciler: 0.33.0(react@19.2.3) + react: 19.2.7 + react-reconciler: 0.33.0(react@19.2.7) signal-exit: 3.0.7 slice-ansi: 7.1.2 stack-utils: 2.0.6 @@ -14024,81 +13775,36 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true - lightningcss-darwin-arm64@1.29.2: - optional: true - lightningcss-darwin-arm64@1.32.0: optional: true - lightningcss-darwin-x64@1.29.2: - optional: true - lightningcss-darwin-x64@1.32.0: optional: true - lightningcss-freebsd-x64@1.29.2: - optional: true - lightningcss-freebsd-x64@1.32.0: optional: true - lightningcss-linux-arm-gnueabihf@1.29.2: - optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: optional: true - lightningcss-linux-arm64-gnu@1.29.2: - optional: true - lightningcss-linux-arm64-gnu@1.32.0: optional: true - lightningcss-linux-arm64-musl@1.29.2: - optional: true - lightningcss-linux-arm64-musl@1.32.0: optional: true - lightningcss-linux-x64-gnu@1.29.2: - optional: true - lightningcss-linux-x64-gnu@1.32.0: optional: true - lightningcss-linux-x64-musl@1.29.2: - optional: true - lightningcss-linux-x64-musl@1.32.0: optional: true - lightningcss-win32-arm64-msvc@1.29.2: - optional: true - lightningcss-win32-arm64-msvc@1.32.0: optional: true - lightningcss-win32-x64-msvc@1.29.2: - optional: true - lightningcss-win32-x64-msvc@1.32.0: optional: true - lightningcss@1.29.2: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-darwin-arm64: 1.29.2 - lightningcss-darwin-x64: 1.29.2 - lightningcss-freebsd-x64: 1.29.2 - lightningcss-linux-arm-gnueabihf: 1.29.2 - lightningcss-linux-arm64-gnu: 1.29.2 - lightningcss-linux-arm64-musl: 1.29.2 - lightningcss-linux-x64-gnu: 1.29.2 - lightningcss-linux-x64-musl: 1.29.2 - lightningcss-win32-arm64-msvc: 1.29.2 - lightningcss-win32-x64-msvc: 1.29.2 - lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -14245,7 +13951,7 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.518.0(react@18.3.1): + lucide-react@0.577.0(react@18.3.1): dependencies: react: 18.3.1 @@ -14789,10 +14495,6 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - mkdirp-classic@0.5.3: optional: true @@ -15291,13 +14993,8 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - postcss-value-parser@4.2.0: {} - - postcss@8.4.49: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 + postcss-value-parser@4.2.0: + optional: true postcss@8.5.15: dependencies: @@ -15509,9 +15206,9 @@ snapshots: transitivePeerDependencies: - supports-color - react-reconciler@0.33.0(react@19.2.3): + react-reconciler@0.33.0(react@19.2.7): dependencies: - react: 19.2.3 + react: 19.2.7 scheduler: 0.27.0 react-refresh@0.18.0: {} @@ -15534,7 +15231,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.31 - react-remove-scroll@2.6.3(@types/react@18.3.31)(react@18.3.1): + react-remove-scroll@2.7.2(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 react-remove-scroll-bar: 2.3.8(@types/react@18.3.31)(react@18.3.1) @@ -15595,7 +15292,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - react@19.2.3: {} + react@19.2.7: {} read-pkg@9.0.1: dependencies: @@ -16036,8 +15733,6 @@ snapshots: dependencies: kind-of: 6.0.3 - shallowequal@1.1.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -16361,21 +16056,17 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-components@6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + styled-components@6.4.3(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@emotion/is-prop-valid': 1.2.2 - '@emotion/unitless': 0.8.1 - '@types/stylis': 4.2.5 - css-to-react-native: 3.2.0 + '@emotion/is-prop-valid': 1.4.0 csstype: 3.1.3 - postcss: 8.4.49 react: 18.3.1 + stylis: 4.3.6 + optionalDependencies: + css-to-react-native: 3.2.0 react-dom: 18.3.1(react@18.3.1) - shallowequal: 1.1.0 - stylis: 4.3.2 - tslib: 2.6.2 - stylis@4.3.2: {} + stylis@4.3.6: {} stylis@4.4.0: {} @@ -16420,15 +16111,15 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwind-merge@3.3.0: {} + tailwind-merge@3.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@4.1.6): + tailwindcss-animate@1.0.7(tailwindcss@4.3.2): dependencies: - tailwindcss: 4.1.6 + tailwindcss: 4.3.2 - tailwindcss@4.1.6: {} + tailwindcss@4.3.2: {} - tapable@2.2.1: {} + tapable@2.3.3: {} tar-fs@3.1.1: dependencies: @@ -16456,15 +16147,6 @@ snapshots: streamx: 2.22.0 optional: true - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - term-size@2.2.1: {} terminal-link@4.0.0: @@ -16571,8 +16253,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.6.2: {} - tslib@2.8.1: {} tsup@8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): @@ -17110,8 +16790,6 @@ snapshots: yallist@4.0.0: {} - yallist@5.0.0: {} - yaml@2.9.0: {} yargs-parser@20.2.9: {} @@ -17192,9 +16870,9 @@ snapshots: zod@3.25.76: {} - zustand@5.0.9(@types/react@18.3.31)(react@19.2.3): + zustand@5.0.9(@types/react@18.3.31)(react@19.2.7): optionalDependencies: '@types/react': 18.3.31 - react: 19.2.3 + react: 19.2.7 zwitch@2.0.4: {} diff --git a/src/package.json b/src/package.json index 4ed4e9df07..406f7a31e1 100644 --- a/src/package.json +++ b/src/package.json @@ -473,7 +473,7 @@ "@roo-code/types": "workspace:^", "ai-sdk-provider-poe": "2.0.18", "async-mutex": "^0.5.0", - "axios": "^1.12.0", + "axios": "^1.18.1", "chokidar": "^4.0.1", "clone-deep": "^4.0.1", "delay": "^6.0.0", @@ -489,7 +489,7 @@ "i18next": "^25.0.0", "ignore": "^7.0.3", "isbinaryfile": "^5.0.7", - "json-stream-stringify": "^3.1.6", + "json-stream-stringify": "^3.1.7", "lodash.debounce": "^4.0.8", "mammoth": "^1.9.1", "monaco-vscode-textmate-theme-converter": "^0.1.7", @@ -504,9 +504,9 @@ "pretty-bytes": "^7.0.0", "proper-lockfile": "^4.1.2", "ps-tree": "^1.2.0", - "reconnecting-eventsource": "^1.6.4", + "reconnecting-eventsource": "^1.6.5", "safe-stable-stringify": "^2.5.0", - "sanitize-filename": "^1.6.3", + "sanitize-filename": "^1.6.4", "say": "^0.16.0", "semver-compare": "^1.0.0", "serialize-error": "^12.0.0", @@ -514,7 +514,7 @@ "simple-git": "^3.27.0", "strip-bom": "^5.0.0", "tiktoken": "^1.0.21", - "tree-sitter-wasms": "^0.1.12", + "tree-sitter-wasms": "^0.1.13", "undici": "^6.21.3", "uuid": "^11.1.0", "vscode-material-icons": "^0.1.1", diff --git a/webview-ui/package.json b/webview-ui/package.json index 13f12626b5..db292e9bc5 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -37,7 +37,7 @@ "@vscode/codicons": "^0.0.45", "@vscode/webview-ui-toolkit": "^1.4.0", "ansi-to-html": "^0.7.2", - "axios": "^1.12.0", + "axios": "^1.18.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -50,7 +50,7 @@ "i18next": "^25.0.0", "katex": "^0.16.11", "lru-cache": "^11.1.0", - "lucide-react": "^0.518.0", + "lucide-react": "^0.577.0", "mermaid": "^11.4.1", "posthog-js": "^1.227.2", "pretty-bytes": "^7.0.0", @@ -67,7 +67,7 @@ "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", - "remove-markdown": "^0.6.0", + "remove-markdown": "^0.6.4", "shell-quote": "^1.8.2", "shiki": "^3.2.1", "source-map": "^0.7.4", diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index af40af4ecf..f973c7929f 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -66,7 +66,7 @@ import { FileCode2, PocketKnife, FolderTree, - TerminalSquare, + SquareTerminal, MessageCircle, Repeat2, Split, @@ -294,7 +294,7 @@ export const ChatRowContent = ({ isCommandExecuting ? ( ) : ( - + ), {t("chat:commandExecution.running")} diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index d3e1af2b08..5742be032c 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -10,7 +10,7 @@ import { VSCodeCheckbox, } from "@vscode/webview-ui-toolkit/react" import * as ProgressPrimitive from "@radix-ui/react-progress" -import { AlertTriangle } from "lucide-react" +import { TriangleAlert } from "lucide-react" import { type IndexingStatus, type EmbedderProvider, CODEBASE_INDEX_DEFAULTS } from "@roo-code/types" @@ -1748,7 +1748,7 @@ export const CodeIndexPopover: React.FC = ({ - + {t("settings:unsavedChangesDialog.title")} diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index cd97c4ab1d..f57203c0dc 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useRef, useState } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { AlertTriangle } from "lucide-react" +import { TriangleAlert } from "lucide-react" import type { ProviderSettingsEntry, OrganizationAllowList } from "@roo-code/types" @@ -246,7 +246,7 @@ const ApiConfigManager = ({ icon: !valid ? ( - + ) : undefined, diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 14d6fa4413..c11a2ab50a 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -16,7 +16,7 @@ import { Database, SquareTerminal, FlaskConical, - AlertTriangle, + TriangleAlert, Globe, Info, MessageSquare, @@ -25,7 +25,7 @@ import { Glasses, Plug, Server, - Users2, + UsersRound, ArrowLeft, GitCommitVertical, GraduationCap, @@ -532,7 +532,7 @@ const SettingsView = forwardRef(({ onDone, t const sections: { id: SectionName; icon: LucideIcon }[] = useMemo( () => [ { id: "providers", icon: Plug }, - { id: "modes", icon: Users2 }, + { id: "modes", icon: UsersRound }, { id: "skills", icon: GraduationCap }, { id: "slashCommands", icon: SquareSlash }, { id: "rules", icon: ScrollText }, @@ -970,7 +970,7 @@ const SettingsView = forwardRef(({ onDone, t - + {t("settings:unsavedChangesDialog.title")} From e07d776af1fc94534464b8f049cb5e5142aa700b Mon Sep 17 00:00:00 2001 From: James Mtendamema <59908268+JamesRobert20@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:15:31 +0300 Subject: [PATCH 18/89] fix(zoo-gateway): enable image attach when model supports vision (#897) * fix(zoo-gateway): enable image attach from live vision model tags Prefer models-API vision tags over the stale hardcoded allowlist so Zoo Gateway (and Vercel AI Gateway) correctly enable image attachment for models like Claude Sonnet 4.5. Co-authored-by: Cursor * fix(zoo-gateway): drop unused eslint-disable that failed CI lint CI does not flag no-var on the ambient global declaration, so the disable became an unused-directive warning. Co-authored-by: Cursor * chore(zoo-gateway): remove redundant vision-capability comments Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../types/src/providers/vercel-ai-gateway.ts | 5 +++ .../__tests__/vercel-ai-gateway.spec.ts | 33 +++++++++++++++++++ .../fetchers/__tests__/zoo-gateway.spec.ts | 22 +++++++++++++ .../providers/fetchers/vercel-ai-gateway.ts | 6 ++-- 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index 38e959649e..a231dd51d3 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -53,12 +53,14 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ "anthropic/claude-3-sonnet", "anthropic/claude-3.5-sonnet", "anthropic/claude-3.7-sonnet", + "anthropic/claude-haiku-4.5", "anthropic/claude-opus-4", "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", + "anthropic/claude-sonnet-4.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "google/gemini-1.5-flash", @@ -85,6 +87,9 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ "openai/gpt-4.5-preview", "openai/gpt-4o", "openai/gpt-4o-mini", + "openai/gpt-5", + "openai/gpt-5-mini", + "openai/gpt-5-nano", "openai/gpt-oss-120b", "openai/gpt-oss-20b", "openai/o3", diff --git a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts index dfb8ad4b3b..28e08d776b 100644 --- a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts @@ -269,6 +269,39 @@ describe("Vercel AI Gateway Fetchers", () => { ) }) + it("prefers live vision tags over hardcoded allowlists", () => { + const taggedVision = parseVercelAiGatewayModel({ + id: "anthropic/claude-sonnet-4.5", + model: { + ...baseModel, + id: "anthropic/claude-sonnet-4.5", + tags: ["tool-use", "vision"], + }, + }) + expect(taggedVision.supportsImages).toBe(true) + + const taggedTextOnly = parseVercelAiGatewayModel({ + id: "anthropic/claude-sonnet-4", + model: { + ...baseModel, + id: "anthropic/claude-sonnet-4", + tags: ["tool-use"], + }, + }) + expect(taggedTextOnly.supportsImages).toBe(false) + }) + + it("falls back to allowlists when tags are absent", () => { + const result = parseVercelAiGatewayModel({ + id: "anthropic/claude-sonnet-4.5", + model: { + ...baseModel, + id: "anthropic/claude-sonnet-4.5", + }, + }) + expect(result.supportsImages).toBe(true) + }) + it("handles missing cache pricing", () => { const modelNoCachePricing = { ...baseModel, diff --git a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts index 0eb4a753dc..ae9bdcc4b1 100644 --- a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts @@ -159,6 +159,28 @@ describe("Zoo Gateway Fetchers", () => { }) describe("parseZooGatewayModel", () => { + it("enables image attachment from Zoo Gateway vision tags", () => { + const result = parseZooGatewayModel({ + id: "anthropic/claude-sonnet-4.5", + model: { + id: "anthropic/claude-sonnet-4.5", + object: "model", + owned_by: "anthropic", + name: "Claude Sonnet 4.5", + context_window: 200000, + max_tokens: 64000, + type: "language", + tags: ["tool-use", "vision"], + pricing: { + input: "3.00", + output: "15.00", + }, + }, + }) + + expect(result.supportsImages).toBe(true) + }) + it("delegates to the vercel-ai-gateway parser", () => { const result = parseZooGatewayModel({ id: "anthropic/claude-sonnet-4", diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts index ba748ddd51..1c4a4279e1 100644 --- a/src/api/providers/fetchers/vercel-ai-gateway.ts +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -34,6 +34,7 @@ const vercelAiGatewayModelSchema = z.object({ context_window: z.number(), max_tokens: z.number(), type: z.string(), + tags: z.array(z.string()).optional(), pricing: vercelAiGatewayPricingSchema, }) @@ -99,8 +100,9 @@ export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: Ve const cacheReadsPrice = model.pricing?.input_cache_read ? parseApiPrice(model.pricing?.input_cache_read) : undefined const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined" - const supportsImages = - VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(id) || VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id) + const supportsImages = Array.isArray(model.tags) + ? model.tags.includes("vision") + : VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(id) || VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id) const modelInfo: ModelInfo = { maxTokens: model.max_tokens, From db2a991c41729180d4fe27bcba612ec6719ffdbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:37:39 +0000 Subject: [PATCH 19/89] chore(deps): update dependency @copilotkit/aimock to v1.35.0 (#820) * chore(deps): update dependency @copilotkit/aimock to v1.35.0 * fix(e2e): update fixtures for aimock v1.35.0 toolCallId breaking change --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- apps/vscode-e2e/fixtures/deepseek-v4.json | 60 ------------------- apps/vscode-e2e/package.json | 2 +- apps/vscode-e2e/src/fixtures/apply-diff.ts | 1 - .../src/fixtures/cold-shell-init.ts | 15 +++-- apps/vscode-e2e/src/fixtures/deepseek-v4.ts | 56 +++++++++++++++++ .../src/fixtures/execute-command.ts | 10 +++- .../src/fixtures/fast-exit-shell-race.ts | 1 - apps/vscode-e2e/src/fixtures/list-files.ts | 1 - .../fixtures/long-running-silent-command.ts | 1 - apps/vscode-e2e/src/fixtures/read-file.ts | 1 - apps/vscode-e2e/src/fixtures/search-files.ts | 1 - .../src/fixtures/terminal-profile.ts | 1 - .../src/fixtures/terminal-reuse-shell-race.ts | 2 - apps/vscode-e2e/src/fixtures/use-mcp-tool.ts | 4 +- apps/vscode-e2e/src/fixtures/write-to-file.ts | 1 - .../src/fixtures/zero-chunk-shell-race.ts | 1 - apps/vscode-e2e/src/runTest.ts | 2 + pnpm-lock.yaml | 12 ++-- 18 files changed, 87 insertions(+), 85 deletions(-) create mode 100644 apps/vscode-e2e/src/fixtures/deepseek-v4.ts diff --git a/apps/vscode-e2e/fixtures/deepseek-v4.json b/apps/vscode-e2e/fixtures/deepseek-v4.json index a4710207e2..597803a323 100644 --- a/apps/vscode-e2e/fixtures/deepseek-v4.json +++ b/apps/vscode-e2e/fixtures/deepseek-v4.json @@ -18,21 +18,6 @@ ] } }, - { - "match": { - "model": "deepseek-v4-flash", - "toolCallId": "call_dsv4_flash_on_read" - }, - "response": { - "toolCalls": [ - { - "name": "attempt_completion", - "arguments": "{\"result\":\"DEEPSEEK_V4_MARKER_deepseek_v4_flash_reasoning_on\"}", - "id": "call_dsv4_flash_on_done" - } - ] - } - }, { "match": { "model": "deepseek-v4-flash", @@ -49,21 +34,6 @@ ] } }, - { - "match": { - "model": "deepseek-v4-flash", - "toolCallId": "call_dsv4_flash_off_read" - }, - "response": { - "toolCalls": [ - { - "name": "attempt_completion", - "arguments": "{\"result\":\"DEEPSEEK_V4_MARKER_deepseek_v4_flash_reasoning_off\"}", - "id": "call_dsv4_flash_off_done" - } - ] - } - }, { "match": { "model": "deepseek-v4-pro", @@ -82,21 +52,6 @@ ] } }, - { - "match": { - "model": "deepseek-v4-pro", - "toolCallId": "call_dsv4_pro_on_read" - }, - "response": { - "toolCalls": [ - { - "name": "attempt_completion", - "arguments": "{\"result\":\"DEEPSEEK_V4_MARKER_deepseek_v4_pro_reasoning_on\"}", - "id": "call_dsv4_pro_on_done" - } - ] - } - }, { "match": { "model": "deepseek-v4-pro", @@ -112,21 +67,6 @@ } ] } - }, - { - "match": { - "model": "deepseek-v4-pro", - "toolCallId": "call_dsv4_pro_off_read" - }, - "response": { - "toolCalls": [ - { - "name": "attempt_completion", - "arguments": "{\"result\":\"DEEPSEEK_V4_MARKER_deepseek_v4_pro_reasoning_off\"}", - "id": "call_dsv4_pro_off_done" - } - ] - } } ] } diff --git a/apps/vscode-e2e/package.json b/apps/vscode-e2e/package.json index 71a25f22ed..8092640de0 100644 --- a/apps/vscode-e2e/package.json +++ b/apps/vscode-e2e/package.json @@ -15,7 +15,7 @@ "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", "@roo-code/types": "workspace:^", - "@copilotkit/aimock": "1.15.1", + "@copilotkit/aimock": "1.35.0", "@types/mocha": "10.0.10", "@types/node": "20.19.43", "@types/vscode": "1.100.0", diff --git a/apps/vscode-e2e/src/fixtures/apply-diff.ts b/apps/vscode-e2e/src/fixtures/apply-diff.ts index 59114af238..adc604391b 100644 --- a/apps/vscode-e2e/src/fixtures/apply-diff.ts +++ b/apps/vscode-e2e/src/fixtures/apply-diff.ts @@ -46,7 +46,6 @@ export function addApplyDiffResultFixtures(mock: InstanceType) { for (const fixture of fixtures) { mock.addFixture({ match: { - toolCallId: fixture.toolCallId, predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/cold-shell-init.ts b/apps/vscode-e2e/src/fixtures/cold-shell-init.ts index d5d25c6cdb..ce618b3422 100644 --- a/apps/vscode-e2e/src/fixtures/cold-shell-init.ts +++ b/apps/vscode-e2e/src/fixtures/cold-shell-init.ts @@ -19,10 +19,17 @@ export function addColdShellInitFixtures(mock: InstanceType) { // second attempt (on the now-warm terminal) captures real output. mock.addFixture({ match: { - toolCallId: "call_cold_shell_init_001", - predicate: (req: ChatCompletionRequest) => - // First attempt returned empty — retry the command - !anyToolResultContains(req, "cold-init-ok"), + // Only retry when the FIRST call (call_cold_shell_init_001) produced an empty result. + // If the retry call (call_cold_shell_init_003) also came back empty, don't loop — + // the tool result message says "Do not run the command again automatically." + predicate: (req: ChatCompletionRequest) => { + const messages: ChatMessage[] = Array.isArray(req?.messages) ? req.messages : [] + const lastToolMsg = messages.filter((m) => m?.role === "tool").at(-1) + return ( + lastToolMsg?.tool_call_id === "call_cold_shell_init_001" && + !anyToolResultContains(req, "cold-init-ok") + ) + }, }, response: { toolCalls: [ diff --git a/apps/vscode-e2e/src/fixtures/deepseek-v4.ts b/apps/vscode-e2e/src/fixtures/deepseek-v4.ts new file mode 100644 index 0000000000..61d22b5ae0 --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/deepseek-v4.ts @@ -0,0 +1,56 @@ +import type { ChatCompletionRequest } from "@copilotkit/aimock" +import { LLMock } from "@copilotkit/aimock" + +// Turn-2 completion fixtures for DeepSeek V4 tests. +// Uses lastToolMsg.tool_call_id to scope each fixture — aimock v1.16.4+ changed +// toolCallId matching to require the very last message to be the tool message, +// but Roo Code appends as a user message after tool results. +const turn2Fixtures = [ + { + toolCallId: "call_dsv4_flash_on_read", + model: "deepseek-v4-flash", + result: "DEEPSEEK_V4_MARKER_deepseek_v4_flash_reasoning_on", + doneId: "call_dsv4_flash_on_done", + }, + { + toolCallId: "call_dsv4_flash_off_read", + model: "deepseek-v4-flash", + result: "DEEPSEEK_V4_MARKER_deepseek_v4_flash_reasoning_off", + doneId: "call_dsv4_flash_off_done", + }, + { + toolCallId: "call_dsv4_pro_on_read", + model: "deepseek-v4-pro", + result: "DEEPSEEK_V4_MARKER_deepseek_v4_pro_reasoning_on", + doneId: "call_dsv4_pro_on_done", + }, + { + toolCallId: "call_dsv4_pro_off_read", + model: "deepseek-v4-pro", + result: "DEEPSEEK_V4_MARKER_deepseek_v4_pro_reasoning_off", + doneId: "call_dsv4_pro_off_done", + }, +] + +export function addDeepSeekV4Fixtures(mock: InstanceType) { + for (const fixture of turn2Fixtures) { + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => { + const messages = Array.isArray(req?.messages) ? req.messages : [] + const lastToolMsg = messages.filter((m) => m?.role === "tool").at(-1) + return req?.model === fixture.model && lastToolMsg?.tool_call_id === fixture.toolCallId + }, + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: fixture.result }), + id: fixture.doneId, + }, + ], + }, + }) + } +} diff --git a/apps/vscode-e2e/src/fixtures/execute-command.ts b/apps/vscode-e2e/src/fixtures/execute-command.ts index dc0e44669a..3adbe7a236 100644 --- a/apps/vscode-e2e/src/fixtures/execute-command.ts +++ b/apps/vscode-e2e/src/fixtures/execute-command.ts @@ -86,8 +86,14 @@ export function addExecuteCommandResultFixtures(mock: InstanceType toolResultContains(req, fixture.toolCallId, fixture.expected), + predicate: (req) => { + const messages = Array.isArray(req?.messages) ? req.messages : [] + const lastToolMsg = messages.filter((m) => m?.role === "tool").at(-1) + return ( + lastToolMsg?.tool_call_id === fixture.toolCallId && + toolResultContains(req, fixture.toolCallId, fixture.expected) + ) + }, }, response: { toolCalls: fixture.toolCalls.map((toolCall) => ({ diff --git a/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts b/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts index a5a92d427b..4b0659a372 100644 --- a/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts +++ b/apps/vscode-e2e/src/fixtures/fast-exit-shell-race.ts @@ -5,7 +5,6 @@ import { toolResultContains } from "./tool-result" export function addFastExitShellRaceResultFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: "call_fast_exit_shell_race_001", // VSCode drops onDidEndTerminalShellExecution for this command (the race under // test), so TerminalProcess.run() only has the D marker itself as proof of // completion, never a real exit code (see ExecuteCommandTool.ts's diff --git a/apps/vscode-e2e/src/fixtures/list-files.ts b/apps/vscode-e2e/src/fixtures/list-files.ts index 9c89147276..d942116ce2 100644 --- a/apps/vscode-e2e/src/fixtures/list-files.ts +++ b/apps/vscode-e2e/src/fixtures/list-files.ts @@ -70,7 +70,6 @@ export function addListFilesResultFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: fixture.toolCallId, predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts b/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts index af9a757c09..7f1e3011a4 100644 --- a/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts +++ b/apps/vscode-e2e/src/fixtures/long-running-silent-command.ts @@ -12,7 +12,6 @@ export function addLongRuningSilentCommandFixtures(mock: InstanceType toolResultContains(req, "call_long_running_silent_001", [ // sleep exits with code 0 — the normal exit status path diff --git a/apps/vscode-e2e/src/fixtures/read-file.ts b/apps/vscode-e2e/src/fixtures/read-file.ts index dd9bb1c10b..18af12d306 100644 --- a/apps/vscode-e2e/src/fixtures/read-file.ts +++ b/apps/vscode-e2e/src/fixtures/read-file.ts @@ -76,7 +76,6 @@ export function addReadFileResultFixtures(mock: InstanceType) { for (const fixture of fixtures) { mock.addFixture({ match: { - toolCallId: fixture.toolCallId, predicate: (req) => isToolResultExpectation(fixture.expected[0]) ? toolResultsContain(req, fixture.expected as ToolResultExpectation[]) diff --git a/apps/vscode-e2e/src/fixtures/search-files.ts b/apps/vscode-e2e/src/fixtures/search-files.ts index 6df43aec58..3c1318c299 100644 --- a/apps/vscode-e2e/src/fixtures/search-files.ts +++ b/apps/vscode-e2e/src/fixtures/search-files.ts @@ -127,7 +127,6 @@ export function addSearchFilesResultFixtures(mock: InstanceType) mock.addFixture({ match: { - toolCallId: fixture.toolCallId, predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/terminal-profile.ts b/apps/vscode-e2e/src/fixtures/terminal-profile.ts index d71f4a572c..79d052a345 100644 --- a/apps/vscode-e2e/src/fixtures/terminal-profile.ts +++ b/apps/vscode-e2e/src/fixtures/terminal-profile.ts @@ -43,7 +43,6 @@ export function addTerminalProfileResultFixtures(mock: InstanceType toolResultContains(req, fixture.toolCallId, fixture.expected), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts b/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts index 01cae1c9f9..2fa9fc25b6 100644 --- a/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts +++ b/apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts @@ -7,7 +7,6 @@ export function addTerminalReuseShellRaceFixtures(mock: InstanceType toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"]), }, response: { @@ -26,7 +25,6 @@ export function addTerminalReuseShellRaceFixtures(mock: InstanceType toolResultContains(req, "call_terminal_reuse_002", ["second", "Exit code: 0"]), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/use-mcp-tool.ts b/apps/vscode-e2e/src/fixtures/use-mcp-tool.ts index 3ce5d0a71c..583e8bdbb3 100644 --- a/apps/vscode-e2e/src/fixtures/use-mcp-tool.ts +++ b/apps/vscode-e2e/src/fixtures/use-mcp-tool.ts @@ -1,5 +1,7 @@ import { LLMock } from "@copilotkit/aimock" +import { toolResultContains } from "./tool-result" + const TEST_DIR_NAME = "use-mcp-tool-fixture" const FILESYSTEM_SERVER_NAME = "filesystem" const READ_FILE_RELATIVE_PATH = `${TEST_DIR_NAME}/mcp-read-target.txt` @@ -99,7 +101,7 @@ export function addUseMcpToolResultFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: fixture.toolCallId, + predicate: (req) => toolResultContains(req, fixture.toolCallId, []), }, response: { toolCalls: [ diff --git a/apps/vscode-e2e/src/fixtures/write-to-file.ts b/apps/vscode-e2e/src/fixtures/write-to-file.ts index d0a31c4db7..457c4ca327 100644 --- a/apps/vscode-e2e/src/fixtures/write-to-file.ts +++ b/apps/vscode-e2e/src/fixtures/write-to-file.ts @@ -31,7 +31,6 @@ export function addWriteToFileResultFixtures(mock: InstanceType) for (const fixture of fixtures) { mock.addFixture({ match: { - toolCallId: fixture.toolCallId, predicate: (req) => toolResultContains(req, fixture.toolCallId, fixture.expected), }, response: { diff --git a/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts b/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts index 0cb0c591cf..3af8119e5b 100644 --- a/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts +++ b/apps/vscode-e2e/src/fixtures/zero-chunk-shell-race.ts @@ -5,7 +5,6 @@ import { toolResultContains } from "./tool-result" export function addZeroChunkShellRaceResultFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: "call_zero_chunk_shell_race_001", // The multiline command is now written to a temp script file and executed // via `sh /tmp/roo-cmd-*.sh` to avoid the VSCode { ... }-wrapping bug that // caused the stream to be closed before read() arrived (zero chunks). diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 92d772e5ad..482e73e945 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -7,6 +7,7 @@ import { runTests } from "@vscode/test-electron" import { LLMock } from "@copilotkit/aimock" import { addApplyDiffResultFixtures } from "./fixtures/apply-diff" +import { addDeepSeekV4Fixtures } from "./fixtures/deepseek-v4" import { addExecuteCommandResultFixtures } from "./fixtures/execute-command" import { addFastExitShellRaceResultFixtures } from "./fixtures/fast-exit-shell-race" import { addZeroChunkShellRaceResultFixtures } from "./fixtures/zero-chunk-shell-race" @@ -127,6 +128,7 @@ async function main() { addSubtaskFixtures(mock) addUseMcpToolResultFixtures(mock) addWriteToFileResultFixtures(mock) + addDeepSeekV4Fixtures(mock) // The modes test (switch_mode → ask) triggers a second API call whose last // user message starts with directly — no diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd950c2178..0f09d80f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,8 +153,8 @@ importers: apps/vscode-e2e: devDependencies: '@copilotkit/aimock': - specifier: 1.15.1 - version: 1.15.1(vitest@4.1.9) + specifier: 1.35.0 + version: 1.35.0(vitest@4.1.9) '@roo-code/config-eslint': specifier: workspace:^ version: link:../../packages/config-eslint @@ -1317,9 +1317,9 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} - '@copilotkit/aimock@1.15.1': - resolution: {integrity: sha512-DG9p6fKdYmuTW0zaUe9iDbgB/CM3SWhpdhVBrszQ6+L2UW4+DZB0gvICFQXRWhVXMpqxEkI9Pqhm/MtMb8li9A==} - engines: {node: '>=24.0.0'} + '@copilotkit/aimock@1.35.0': + resolution: {integrity: sha512-dWtNUeaWD4jaC3Kv9oBiCoIczENM6u8pWBCmybn0/hSFEY57yqCtOGPsVNIG/YlmwbH8p5sgtL6xZzo1FnPi8g==} + engines: {node: '>=20.15.0'} hasBin: true peerDependencies: jest: '>=29' @@ -9058,7 +9058,7 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@copilotkit/aimock@1.15.1(vitest@4.1.9)': + '@copilotkit/aimock@1.35.0(vitest@4.1.9)': optionalDependencies: vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) From ce39ee5872ae5b6f99b2d521d279e2607e3b6032 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:57:51 -0400 Subject: [PATCH 20/89] [Chore] Add weekly Discord release reminder (#900) * chore: add weekly Discord release reminder * ci(release-reminder): add concurrency guard, failure notification, and shell hardening --------- Co-authored-by: Roomote Co-authored-by: Elliott de Launay --- .github/workflows/release-reminder.yml | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/release-reminder.yml diff --git a/.github/workflows/release-reminder.yml b/.github/workflows/release-reminder.yml new file mode 100644 index 0000000000..6f5b6b8ea6 --- /dev/null +++ b/.github/workflows/release-reminder.yml @@ -0,0 +1,68 @@ +name: Post weekly release reminder + +# Requires repo secret DISCORD_RELEASE_WEBHOOK_URL — Discord webhook for the +# release-reminders channel. The reminder job fails (noisily) if it is unset. + +on: + schedule: + # Friday at 09:00 UTC + - cron: "0 9 * * 5" + workflow_dispatch: + +permissions: {} + +concurrency: + group: release-reminder + # A re-run produces an identical message, so cancel a stale/duplicate run + # rather than risk posting twice. + cancel-in-progress: true + +jobs: + remind: + runs-on: ubuntu-latest + steps: + - name: Post release reminder to Discord + env: + DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} + run: | + set -euo pipefail + owners=("Elliott" "Navad" "Toray") + anchor_date="2026-07-17" + seconds_since_anchor=$(($(date -u +%s) - $(date -u -d "$anchor_date" +%s))) + weeks_since_anchor=$((seconds_since_anchor / 604800)) + owner="${owners[$((weeks_since_anchor % ${#owners[@]}))]}" + + message=$(cat < Date: Wed, 15 Jul 2026 19:58:53 -0400 Subject: [PATCH 21/89] chore(deps): update dependency zustand to v5.0.14 (#815) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c720b1842f..2e05d14e26 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -35,7 +35,7 @@ "p-wait-for": "^5.0.2", "react": "^19.1.0", "superjson": "^2.2.6", - "zustand": "^5.0.0" + "zustand": "^5.0.14" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f09d80f4e..24fae650ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,8 +116,8 @@ importers: specifier: ^2.2.6 version: 2.2.6 zustand: - specifier: ^5.0.0 - version: 5.0.9(@types/react@18.3.31)(react@19.2.7) + specifier: ^5.0.14 + version: 5.0.14(@types/react@18.3.31)(react@19.2.7) devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -8380,8 +8380,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zustand@5.0.9: - resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': 18.3.31 @@ -16870,7 +16870,7 @@ snapshots: zod@3.25.76: {} - zustand@5.0.9(@types/react@18.3.31)(react@19.2.7): + zustand@5.0.14(@types/react@18.3.31)(react@19.2.7): optionalDependencies: '@types/react': 18.3.31 react: 19.2.7 From 5959abf37affab3428eadd40839f5d889548e0d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:05:00 -0400 Subject: [PATCH 22/89] chore(deps): update dependency @eslint/js to v9.39.4 (#822) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/config-eslint/package.json | 2 +- pnpm-lock.yaml | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index b5be1b4b57..f9fc752f32 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -8,7 +8,7 @@ "./next-js": "./next.js" }, "devDependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "9.39.4", "@next/eslint-plugin-next": "15.3.2", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24fae650ae..71e0162ebe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,8 +248,8 @@ importers: packages/config-eslint: devDependencies: '@eslint/js': - specifier: 9.27.0 - version: 9.27.0 + specifier: 9.39.4 + version: 9.39.4 '@next/eslint-plugin-next': specifier: 15.3.2 version: 15.3.2 @@ -1571,10 +1571,6 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.27.0': - resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.4': resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9247,8 +9243,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.27.0': {} - '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} From d25926cc806557f0e0d1baf3650c1280b31faef0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:19:37 +0000 Subject: [PATCH 23/89] chore(deps): update dependency @next/eslint-plugin-next to v15.5.20 (#823) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/config-eslint/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index f9fc752f32..8c10849f33 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@eslint/js": "9.39.4", - "@next/eslint-plugin-next": "15.3.2", + "@next/eslint-plugin-next": "15.5.20", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", "eslint-plugin-only-warn": "1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71e0162ebe..e845570c9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,8 +251,8 @@ importers: specifier: 9.39.4 version: 9.39.4 '@next/eslint-plugin-next': - specifier: 15.3.2 - version: 15.3.2 + specifier: 15.5.20 + version: 15.5.20 eslint: specifier: 9.39.4 version: 9.39.4(jiti@2.7.0) @@ -1757,8 +1757,8 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/eslint-plugin-next@15.3.2': - resolution: {integrity: sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==} + '@next/eslint-plugin-next@15.5.20': + resolution: {integrity: sha512-MZUgFpVd9rGSZpb8bNceUWvkAZe6aQw/6h2SSqHQuYzKfaiEUEVPIO6mXqaBmiBEBSN1f1sOc1uV8GCM90oegg==} '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} @@ -9516,7 +9516,7 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/eslint-plugin-next@15.3.2': + '@next/eslint-plugin-next@15.5.20': dependencies: fast-glob: 3.3.1 From dba8f8bfd3d7abb53006531e25aa89d30b8c5340 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:31:49 -0400 Subject: [PATCH 24/89] chore(deps): update dependency @dotenvx/dotenvx to v1.75.1 (#821) * chore(deps): update dependency @dotenvx/dotenvx to v1.75.1 * chore: replace @dotenvx/dotenvx with plain dotenv --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- apps/vscode-e2e/package.json | 3 +- package.json | 1 - pnpm-lock.yaml | 175 ++++++-------------------------- src/__tests__/extension.spec.ts | 14 +-- src/extension.ts | 6 +- src/package.json | 1 + 6 files changed, 43 insertions(+), 157 deletions(-) diff --git a/apps/vscode-e2e/package.json b/apps/vscode-e2e/package.json index 8092640de0..2662e7cf07 100644 --- a/apps/vscode-e2e/package.json +++ b/apps/vscode-e2e/package.json @@ -8,7 +8,7 @@ "test:ci": "pnpm -w bundle && pnpm --filter @roo-code/vscode-webview build && pnpm test:run", "test:ci:mock": "pnpm -w bundle && pnpm --filter @roo-code/vscode-webview build && USE_MOCK=true pnpm test:run", "test:record": "AIMOCK_RECORD=true pnpm test:ci", - "test:run": "rimraf out && tsc -p tsconfig.json && npx dotenvx run -f .env.local -- node ./out/runTest.js", + "test:run": "rimraf out && tsc -p tsconfig.json && dotenv -e .env.local -- node ./out/runTest.js", "clean": "rimraf out .turbo" }, "devDependencies": { @@ -20,6 +20,7 @@ "@types/node": "20.19.43", "@types/vscode": "1.100.0", "@vscode/test-electron": "2.5.2", + "dotenv-cli": "11.0.0", "glob": "11.1.0", "mocha": "11.2.2", "rimraf": "6.0.1" diff --git a/package.json b/package.json index 125ecc7c01..5a61224b0c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ }, "devDependencies": { "@changesets/cli": "2.31.0", - "@dotenvx/dotenvx": "1.66.0", "@roo-code/config-typescript": "workspace:^", "@types/node": "20.19.43", "@vscode/vsce": "3.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e845570c9b..42ac84d18c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,9 +25,6 @@ importers: '@changesets/cli': specifier: 2.31.0 version: 2.31.0(@types/node@20.19.43) - '@dotenvx/dotenvx': - specifier: 1.66.0 - version: 1.66.0 '@roo-code/config-typescript': specifier: workspace:^ version: link:packages/config-typescript @@ -176,6 +173,9 @@ importers: '@vscode/test-electron': specifier: 2.5.2 version: 2.5.2 + dotenv-cli: + specifier: 11.0.0 + version: 11.0.0 glob: specifier: 11.1.0 version: 11.1.0 @@ -484,6 +484,9 @@ importers: diff: specifier: ^5.2.0 version: 5.2.2 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 exceljs: specifier: ^4.4.0 version: 4.4.0 @@ -1358,16 +1361,6 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@dotenvx/dotenvx@1.66.0': - resolution: {integrity: sha512-qlQFhHUjhRDybrinqLAD0MClVZDOrsq80O8eD5iSjz3Qa/4f3Jg7SQrOaSobrRyP1QaWIYLGtGpj2c7H0D8NUw==} - hasBin: true - - '@ecies/ciphers@0.2.6': - resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} - engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} - peerDependencies: - '@noble/ciphers': ^1.0.0 - '@emnapi/core@1.11.0': resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} @@ -1760,18 +1753,6 @@ packages: '@next/eslint-plugin-next@15.5.20': resolution: {integrity: sha512-MZUgFpVd9rGSZpb8bNceUWvkAZe6aQw/6h2SSqHQuYzKfaiEUEVPIO6mXqaBmiBEBSN1f1sOc1uV8GCM90oegg==} - '@noble/ciphers@1.3.0': - resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} - engines: {node: ^14.21.3 || >=16} - - '@noble/curves@1.9.7': - resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} - engines: {node: ^14.21.3 || >=16} - - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - '@node-rs/crc32-android-arm-eabi@1.10.6': resolution: {integrity: sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ==} engines: {node: '>= 10'} @@ -3921,10 +3902,6 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -4374,10 +4351,22 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv-cli@11.0.0: + resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} + hasBin: true + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -4405,10 +4394,6 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - eciesjs@0.4.18: - resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} - engines: {bun: '>=1', deno: '>=2', node: '>=16'} - editions@6.22.0: resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==} engines: {ecmascript: '>= es5', node: '>=4'} @@ -4640,10 +4625,6 @@ packages: resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} engines: {node: '>=8.3.0'} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -4913,10 +4894,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -5120,10 +5097,6 @@ packages: resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} hasBin: true - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -5409,10 +5382,6 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5489,10 +5458,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.5: - resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} - engines: {node: '>=18'} - isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -6342,10 +6307,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6372,10 +6333,6 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - object-treeify@1.1.33: - resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} - engines: {node: '>= 10'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -7492,10 +7449,6 @@ packages: resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} engines: {node: '>=12'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -8212,11 +8165,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -8347,10 +8295,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - yocto-spinner@1.2.0: - resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==} - engines: {node: '>=18.19'} - yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -9078,23 +9022,6 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@dotenvx/dotenvx@1.66.0': - dependencies: - commander: 11.1.0 - dotenv: 17.4.2 - eciesjs: 0.4.18 - execa: 5.1.1 - fdir: 6.5.0(picomatch@4.0.4) - ignore: 5.3.2 - object-treeify: 1.1.33 - picomatch: 4.0.4 - which: 4.0.0 - yocto-spinner: 1.2.0 - - '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': - dependencies: - '@noble/ciphers': 1.3.0 - '@emnapi/core@1.11.0': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -9520,14 +9447,6 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@noble/ciphers@1.3.0': {} - - '@noble/curves@1.9.7': - dependencies: - '@noble/hashes': 1.8.0 - - '@noble/hashes@1.8.0': {} - '@node-rs/crc32-android-arm-eabi@1.10.6': optional: true @@ -11713,8 +11632,6 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@11.1.0: {} - commander@12.1.0: {} commander@14.0.3: {} @@ -12166,8 +12083,21 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv-cli@11.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 17.4.2 + dotenv-expand: 12.0.3 + minimist: 1.2.8 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + dotenv@16.0.3: {} + dotenv@16.6.1: {} + dotenv@17.4.2: {} duck@0.1.12: @@ -12194,13 +12124,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - eciesjs@0.4.18: - dependencies: - '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 - editions@6.22.0: dependencies: version-range: 4.15.0 @@ -12544,18 +12467,6 @@ snapshots: unzipper: 0.10.14 uuid: 8.3.2 - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -12881,8 +12792,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@6.0.1: {} - get-stream@8.0.1: {} get-stream@9.0.1: @@ -13174,8 +13083,6 @@ snapshots: human-id@4.1.2: {} - human-signals@2.1.0: {} - human-signals@5.0.0: {} human-signals@8.0.1: {} @@ -13428,8 +13335,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-stream@2.0.1: {} - is-stream@3.0.0: {} is-stream@4.0.1: {} @@ -13488,8 +13393,6 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.5: {} - isobject@3.0.1: {} istanbul-lib-coverage@3.2.2: {} @@ -14627,10 +14530,6 @@ snapshots: normalize-path@3.0.0: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -14652,8 +14551,6 @@ snapshots: object-keys@1.1.1: {} - object-treeify@1.1.33: {} - object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -16013,8 +15910,6 @@ snapshots: strip-bom@5.0.0: {} - strip-final-newline@2.0.0: {} - strip-final-newline@3.0.0: {} strip-final-newline@4.0.0: {} @@ -16718,10 +16613,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@4.0.0: - dependencies: - isexe: 3.1.5 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -16840,10 +16731,6 @@ snapshots: yocto-queue@1.2.1: {} - yocto-spinner@1.2.0: - dependencies: - yoctocolors: 2.1.2 - yoctocolors@2.1.2: {} yoga-layout@3.2.1: {} diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index 689e35307d..8af352425a 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -42,7 +42,7 @@ vi.mock("vscode", () => ({ }, })) -vi.mock("@dotenvx/dotenvx", () => ({ +vi.mock("dotenv", () => ({ config: vi.fn(), })) @@ -237,34 +237,34 @@ describe("extension.ts", () => { authStateChangedHandler = undefined }) - test("does not call dotenvx.config when optional .env does not exist", async () => { + test("does not call dotenv.config when optional .env does not exist", async () => { vi.resetModules() vi.clearAllMocks() const fs = await import("fs") vi.mocked(fs.existsSync).mockReturnValue(false) - const dotenvx = await import("@dotenvx/dotenvx") + const dotenv = await import("dotenv") const { activate } = await import("../extension") await activate(mockContext) - expect(dotenvx.config).not.toHaveBeenCalled() + expect(dotenv.config).not.toHaveBeenCalled() }) - test("calls dotenvx.config when optional .env exists", async () => { + test("calls dotenv.config when optional .env exists", async () => { vi.resetModules() vi.clearAllMocks() const fs = await import("fs") vi.mocked(fs.existsSync).mockReturnValue(true) - const dotenvx = await import("@dotenvx/dotenvx") + const dotenv = await import("dotenv") const { activate } = await import("../extension") await activate(mockContext) - expect(dotenvx.config).toHaveBeenCalledTimes(1) + expect(dotenv.config).toHaveBeenCalledTimes(1) }) describe("cloud auth state handling", () => { diff --git a/src/extension.ts b/src/extension.ts index e326509c3d..f55aa61405 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,16 +1,14 @@ import * as vscode from "vscode" -import * as dotenvx from "@dotenvx/dotenvx" +import * as dotenv from "dotenv" import * as fs from "fs" import * as path from "path" // Load environment variables from .env file // The extension-level .env is optional (not shipped in production builds). -// Avoid calling dotenvx when the file doesn't exist, otherwise dotenvx emits -// a noisy [MISSING_ENV_FILE] error to the extension host console. const envPath = path.join(__dirname, "..", ".env") if (fs.existsSync(envPath)) { try { - dotenvx.config({ path: envPath }) + dotenv.config({ path: envPath }) } catch (e) { // Best-effort only: never fail extension activation due to optional env loading. console.warn("Failed to load environment variables:", e) diff --git a/src/package.json b/src/package.json index 406f7a31e1..020705effc 100644 --- a/src/package.json +++ b/src/package.json @@ -478,6 +478,7 @@ "clone-deep": "^4.0.1", "delay": "^6.0.0", "diff": "^5.2.0", + "dotenv": "^17.4.2", "exceljs": "^4.4.0", "fast-deep-equal": "^3.1.3", "fastest-levenshtein": "^1.0.16", From e6f2a0eafcef41b026edb397e6ec86d0a276dafa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:56:54 -0400 Subject: [PATCH 25/89] chore(deps): update dependency date-fns to v4.4.0 (#828) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42ac84d18c..e806f5621a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -772,7 +772,7 @@ importers: version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: ^4.1.0 - version: 4.1.0 + version: 4.4.0 debounce: specifier: ^2.1.1 version: 2.2.0 @@ -4210,8 +4210,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -11966,7 +11966,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@4.1.0: {} + date-fns@4.4.0: {} dayjs@1.11.21: {} From 4a40c698706930ab32860dba7506b0777b7c2adc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:05:06 +0000 Subject: [PATCH 26/89] chore(deps): update dependency @vscode/ripgrep to v1.18.0 (#826) * chore(deps): update dependency @vscode/ripgrep to v1.18.0 * fix(cli): resolve ripgrep binary from per-platform package in build.sh --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- apps/cli/scripts/build.sh | 2 +- pnpm-lock.yaml | 143 ++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 31 deletions(-) diff --git a/apps/cli/scripts/build.sh b/apps/cli/scripts/build.sh index fae70473df..c9c95fe898 100755 --- a/apps/cli/scripts/build.sh +++ b/apps/cli/scripts/build.sh @@ -173,7 +173,7 @@ create_tarball() { # Find and copy ripgrep binary info "Looking for ripgrep binary..." - RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep/bin/rg" -type f 2>/dev/null | head -1) + RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep*/bin/rg" -type f 2>/dev/null | head -1) if [ -n "$RIPGREP_PATH" ] && [ -f "$RIPGREP_PATH" ]; then info "Found ripgrep at: $RIPGREP_PATH" mkdir -p "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e806f5621a..097a16353f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,7 +90,7 @@ importers: version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) '@vscode/ripgrep': specifier: ^1.15.9 - version: 1.17.0 + version: 1.18.0 commander: specifier: ^12.1.0 version: 12.1.0 @@ -3331,8 +3331,68 @@ packages: '@vscode/codicons@0.0.45': resolution: {integrity: sha512-1KAZ7XCMagp5Gdrlr4bbbcAqgcIL623iO1wW6rfcSVGAVUQvR0WP7bQx1SbJ11gmV3fdQTSEFIJQ/5C+HuVasw==} - '@vscode/ripgrep@1.17.0': - resolution: {integrity: sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==} + '@vscode/ripgrep-darwin-arm64@1.18.0': + resolution: {integrity: sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==} + cpu: [arm64] + os: [darwin] + + '@vscode/ripgrep-darwin-x64@1.18.0': + resolution: {integrity: sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==} + cpu: [x64] + os: [darwin] + + '@vscode/ripgrep-linux-arm64@1.18.0': + resolution: {integrity: sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==} + cpu: [arm64] + os: [linux] + + '@vscode/ripgrep-linux-arm@1.18.0': + resolution: {integrity: sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==} + cpu: [arm] + os: [linux] + + '@vscode/ripgrep-linux-ia32@1.18.0': + resolution: {integrity: sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==} + cpu: [ia32] + os: [linux] + + '@vscode/ripgrep-linux-ppc64@1.18.0': + resolution: {integrity: sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==} + cpu: [ppc64] + os: [linux] + + '@vscode/ripgrep-linux-riscv64@1.18.0': + resolution: {integrity: sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==} + cpu: [riscv64] + os: [linux] + + '@vscode/ripgrep-linux-s390x@1.18.0': + resolution: {integrity: sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==} + cpu: [s390x] + os: [linux] + + '@vscode/ripgrep-linux-x64@1.18.0': + resolution: {integrity: sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==} + cpu: [x64] + os: [linux] + + '@vscode/ripgrep-win32-arm64@1.18.0': + resolution: {integrity: sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==} + cpu: [arm64] + os: [win32] + + '@vscode/ripgrep-win32-ia32@1.18.0': + resolution: {integrity: sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==} + cpu: [ia32] + os: [win32] + + '@vscode/ripgrep-win32-x64@1.18.0': + resolution: {integrity: sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==} + cpu: [x64] + os: [win32] + + '@vscode/ripgrep@1.18.0': + resolution: {integrity: sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==} '@vscode/test-electron@2.5.2': resolution: {integrity: sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==} @@ -4715,9 +4775,6 @@ packages: fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -6726,9 +6783,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -8277,9 +8331,6 @@ packages: resolution: {integrity: sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA==} engines: {node: '>=16'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yauzl@3.4.0: resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} @@ -10988,13 +11039,56 @@ snapshots: '@vscode/codicons@0.0.45': {} - '@vscode/ripgrep@1.17.0': - dependencies: - https-proxy-agent: 7.0.6 - proxy-from-env: 1.1.0 - yauzl: 2.10.0 - transitivePeerDependencies: - - supports-color + '@vscode/ripgrep-darwin-arm64@1.18.0': + optional: true + + '@vscode/ripgrep-darwin-x64@1.18.0': + optional: true + + '@vscode/ripgrep-linux-arm64@1.18.0': + optional: true + + '@vscode/ripgrep-linux-arm@1.18.0': + optional: true + + '@vscode/ripgrep-linux-ia32@1.18.0': + optional: true + + '@vscode/ripgrep-linux-ppc64@1.18.0': + optional: true + + '@vscode/ripgrep-linux-riscv64@1.18.0': + optional: true + + '@vscode/ripgrep-linux-s390x@1.18.0': + optional: true + + '@vscode/ripgrep-linux-x64@1.18.0': + optional: true + + '@vscode/ripgrep-win32-arm64@1.18.0': + optional: true + + '@vscode/ripgrep-win32-ia32@1.18.0': + optional: true + + '@vscode/ripgrep-win32-x64@1.18.0': + optional: true + + '@vscode/ripgrep@1.18.0': + optionalDependencies: + '@vscode/ripgrep-darwin-arm64': 1.18.0 + '@vscode/ripgrep-darwin-x64': 1.18.0 + '@vscode/ripgrep-linux-arm': 1.18.0 + '@vscode/ripgrep-linux-arm64': 1.18.0 + '@vscode/ripgrep-linux-ia32': 1.18.0 + '@vscode/ripgrep-linux-ppc64': 1.18.0 + '@vscode/ripgrep-linux-riscv64': 1.18.0 + '@vscode/ripgrep-linux-s390x': 1.18.0 + '@vscode/ripgrep-linux-x64': 1.18.0 + '@vscode/ripgrep-win32-arm64': 1.18.0 + '@vscode/ripgrep-win32-ia32': 1.18.0 + '@vscode/ripgrep-win32-x64': 1.18.0 '@vscode/test-electron@2.5.2': dependencies: @@ -12603,10 +12697,6 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -14991,8 +15081,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} - proxy-from-env@2.1.0: {} ps-tree@1.2.0: @@ -16714,11 +16802,6 @@ snapshots: is-it-type: 5.1.2 simple-invariant: 2.0.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yauzl@3.4.0: dependencies: pend: 1.2.0 From 1a46e0594cadf6647df3844623341e67af4527db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:39:46 +0000 Subject: [PATCH 27/89] chore(deps): update dependency @tanstack/react-query to v5.101.2 (#824) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 097a16353f..8350c8d74d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -748,7 +748,7 @@ importers: version: 4.3.2(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@tanstack/react-query': specifier: ^5.68.0 - version: 5.76.1(react@18.3.1) + version: 5.101.2(react@18.3.1) '@vscode/codicons': specifier: ^0.0.45 version: 0.0.45 @@ -2865,11 +2865,11 @@ packages: peerDependencies: vite: 8.1.0 - '@tanstack/query-core@5.76.0': - resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/react-query@5.76.1': - resolution: {integrity: sha512-YxdLZVGN4QkT5YT1HKZQWiIlcgauIXEIsMOTSjvyD5wLYK8YVvKZUPAysMqossFJJfDpJW3pFn7WNZuPOqq+fw==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -10484,11 +10484,11 @@ snapshots: tailwindcss: 4.3.2 vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - '@tanstack/query-core@5.76.0': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/react-query@5.76.1(react@18.3.1)': + '@tanstack/react-query@5.101.2(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.76.0 + '@tanstack/query-core': 5.101.2 react: 18.3.1 '@testing-library/dom@10.4.0': From f645f02aac8fad1ac70dd9573ba4e5926ab224eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:43:30 -0400 Subject: [PATCH 28/89] chore(deps): update dependency csstype to v3.2.3 (#827) * chore(deps): update dependency csstype to v3.2.3 * fix(e2e): wait for reopened parent before draining stack in delegation test --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- apps/vscode-e2e/src/suite/subtasks.test.ts | 13 ++++++++++--- package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 95d74970b9..05b8178b1f 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -241,10 +241,17 @@ suite("Roo Code Subtasks", function () { } finally { api.off(RooCodeEventName.Message, messageHandler) api.off(RooCodeEventName.TaskDelegationCompleted, delegationCompletedHandler) - if (api.getCurrentTaskStack().length > 0) { - await api.clearCurrentTask() + // TaskDelegationCompleted fires before createTaskWithHistoryItem completes, + // so the reopened parent may not be on the stack yet. Wait for it to appear + // before draining, otherwise the next test sees a late-rehydrated stray task. + if (delegationCompletedParentId) { + await waitFor( + () => + api.getCurrentTaskStack().length > 0 || + api.getCurrentTaskStack().includes(delegationCompletedParentId!), + ).catch(() => {}) } - if (api.getCurrentTaskStack().length > 0) { + while (api.getCurrentTaskStack().length > 0) { await api.clearCurrentTask() } await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) diff --git a/package.json b/package.json index 5a61224b0c..2c1dd77639 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "glob": "11.1.0", "@types/react": "18.3.31", "@types/react-dom": "18.3.7", - "csstype": "3.1.3", + "csstype": "3.2.3", "zod": "3.25.76" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8350c8d74d..e626a22c58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ overrides: glob: 11.1.0 '@types/react': 18.3.31 '@types/react-dom': 18.3.7 - csstype: 3.1.3 + csstype: 3.2.3 zod: 3.25.76 importers: @@ -4091,8 +4091,8 @@ packages: resolution: {integrity: sha512-W0Y2HOXlPkb2yaKrCVRjinYKciu/qSLEmK0K9mcfDei3zwlnHFEHAs/Du3cIRwPqY+J4JsiBzUjoHyc8RsJ03A==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} cytoscape-cose-bilkent@4.1.0: resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} @@ -10830,7 +10830,7 @@ snapshots: '@types/react@18.3.31': dependencies: '@types/prop-types': 15.7.14 - csstype: 3.1.3 + csstype: 3.2.3 '@types/retry@0.12.0': {} @@ -11849,7 +11849,7 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} + csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): dependencies: @@ -14547,7 +14547,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 css-tree: 1.1.3 - csstype: 3.1.3 + csstype: 3.2.3 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.1 react: 18.3.1 @@ -16036,7 +16036,7 @@ snapshots: styled-components@6.4.3(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@emotion/is-prop-valid': 1.4.0 - csstype: 3.1.3 + csstype: 3.2.3 react: 18.3.1 stylis: 4.3.6 optionalDependencies: From c39535e3949e6b7756be7489bf3f755cecd59ad0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:44:06 +0000 Subject: [PATCH 29/89] chore(deps): update dependency @trpc/client to v11.18.0 (#825) * chore(deps): update dependency @trpc/client to v11.18.0 * chore(deps): bump @trpc/server to v11.18.0 to satisfy @trpc/client peer dep * fix(webview): remove ineffective memo wrapper on SettingsView --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- apps/cli/package.json | 3 ++- pnpm-lock.yaml | 25 +++++++++++-------- .../src/components/settings/SettingsView.tsx | 3 +-- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 2e05d14e26..04a59a8162 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -26,7 +26,7 @@ "@roo-code/core": "workspace:^", "@roo-code/types": "workspace:^", "@roo-code/vscode-shim": "workspace:^", - "@trpc/client": "^11.8.1", + "@trpc/client": "^11.18.0", "@vscode/ripgrep": "^1.15.9", "commander": "^12.1.0", "execa": "^9.5.2", @@ -39,6 +39,7 @@ }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", + "@trpc/server": "^11.18.0", "@roo-code/config-typescript": "workspace:^", "@types/node": "20.19.43", "@types/react": "18.3.31", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e626a22c58..17de8c1b34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,8 +86,8 @@ importers: specifier: workspace:^ version: link:../../packages/vscode-shim '@trpc/client': - specifier: ^11.8.1 - version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.18.0 + version: 11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3) '@vscode/ripgrep': specifier: ^1.15.9 version: 1.18.0 @@ -122,6 +122,9 @@ importers: '@roo-code/config-typescript': specifier: workspace:^ version: link:../../packages/config-typescript + '@trpc/server': + specifier: ^11.18.0 + version: 11.18.0(typescript@5.9.3) '@types/node': specifier: 20.19.43 version: 20.19.43 @@ -2917,14 +2920,16 @@ packages: '@textlint/types@15.7.1': resolution: {integrity: sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==} - '@trpc/client@11.8.1': - resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==} + '@trpc/client@11.18.0': + resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==} + hasBin: true peerDependencies: - '@trpc/server': 11.8.1 + '@trpc/server': 11.18.0 typescript: '>=5.7.2' - '@trpc/server@11.8.1': - resolution: {integrity: sha512-P4rzZRpEL7zDFgjxK65IdyH0e41FMFfTkQkuq0BA5tKcr7E6v9/v38DEklCpoDN6sPiB1Sigy/PUEzHENhswDA==} + '@trpc/server@11.18.0': + resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} + hasBin: true peerDependencies: typescript: '>=5.7.2' @@ -10554,12 +10559,12 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.7.1 - '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)': + '@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@trpc/server': 11.8.1(typescript@5.9.3) + '@trpc/server': 11.18.0(typescript@5.9.3) typescript: 5.9.3 - '@trpc/server@11.8.1(typescript@5.9.3)': + '@trpc/server@11.18.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index c11a2ab50a..87052a21a2 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,6 +1,5 @@ import React, { forwardRef, - memo, useCallback, useEffect, useImperativeHandle, @@ -991,4 +990,4 @@ const SettingsView = forwardRef(({ onDone, t ) }) -export default memo(SettingsView) +export default SettingsView From 537cede4970e1ea1a895c8d98825c97d15b37e63 Mon Sep 17 00:00:00 2001 From: Naved Merchant Date: Thu, 16 Jul 2026 16:43:07 -0700 Subject: [PATCH 30/89] feat: add Grok 4.5 model support and fix xAI reasoning format (#867) Add grok-4.5 model definition with 500K context window, .00/.00 input/output pricing, /bin/sh.50 cache pricing, and configurable reasoning (low/medium/high, default high). Update xaiDefaultModelId from grok-4.20 to grok-4.5 as the new flagship model. Fix a latent bug in the xAI handler where reasoning effort was sent in Chat Completions format (reasoning: { reasoning_effort }) instead of the Responses API format (reasoning: { effort }). This affected grok-3-mini and would have broken grok-4.5's configurable reasoning. Update tests to cover the reasoning format fix and add grok-4.5 reasoning support tests. Update e2e test primary model to grok-4.5. Fixes #866 --- .../src/suite/providers/xai.test.ts | 4 +- packages/types/src/providers/xai.ts | 18 +++++++- src/api/providers/__tests__/xai.spec.ts | 45 ++++++++++++++++++- src/api/providers/xai.ts | 6 ++- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/apps/vscode-e2e/src/suite/providers/xai.test.ts b/apps/vscode-e2e/src/suite/providers/xai.test.ts index 013f8f55a5..7403d71279 100644 --- a/apps/vscode-e2e/src/suite/providers/xai.test.ts +++ b/apps/vscode-e2e/src/suite/providers/xai.test.ts @@ -12,7 +12,7 @@ const XAI_API_KEY = process.env.XAI_API_KEY const XAI_BASE_URL = "https://api.x.ai/v1" const XAI_RESPONSES_URL = `${XAI_BASE_URL}/responses` // Primary model for the full round-trip test (completion-text assertion included). -const XAI_MODEL_ID = "grok-4.20" +const XAI_MODEL_ID = "grok-4.5" // Fast variants: tested for API parameter contract only. They consistently call // attempt_completion with an empty result field after a no-tool-error recovery // loop, so they cannot satisfy the completion-text assertion at this time. @@ -492,7 +492,7 @@ suite("xAI provider", function () { const readCallId = modelFixture?.readCallId ?? "call_xai_read_001" if (request.functionCallOutputIds.some((id) => id === readCallId)) { - // Use recorded turn2 when it contains a function_call (grok-4.20). + // Use recorded turn2 when it contains a function_call (grok-4.5). // Fast models return plain text in turn2 — hand-craft attempt_completion // so the task can reach completion. const turn2HasFunctionCall = (modelFixture?.turn2 as ResponsesStreamEvent[] | undefined)?.some( diff --git a/packages/types/src/providers/xai.ts b/packages/types/src/providers/xai.ts index a80e0d7357..9e83dd4e18 100644 --- a/packages/types/src/providers/xai.ts +++ b/packages/types/src/providers/xai.ts @@ -3,9 +3,25 @@ import type { ModelInfo } from "../model.js" // https://docs.x.ai/docs/api-reference export type XAIModelId = keyof typeof xaiModels -export const xaiDefaultModelId: XAIModelId = "grok-4.20" +export const xaiDefaultModelId: XAIModelId = "grok-4.5" export const xaiModels = { + "grok-4.5": { + maxTokens: 65_536, + contextWindow: 500_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.0, + outputPrice: 6.0, + cacheWritesPrice: 0.5, + cacheReadsPrice: 0.5, + description: + "xAI's flagship Grok 4.5 model with 500K context, configurable reasoning (low/medium/high), and agentic tool calling via Responses API.", + supportsReasoningEffort: ["low", "medium", "high"], + reasoningEffort: "high", + includedTools: ["search_replace"], + excludedTools: ["apply_diff"], + }, "grok-4.20": { maxTokens: 65_536, contextWindow: 2_000_000, diff --git a/src/api/providers/__tests__/xai.spec.ts b/src/api/providers/__tests__/xai.spec.ts index 78f091b8e3..794faa5ebc 100644 --- a/src/api/providers/__tests__/xai.spec.ts +++ b/src/api/providers/__tests__/xai.spec.ts @@ -255,7 +255,7 @@ describe("XAIHandler", () => { await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`) }) - it("should include reasoning_effort for mini models", async () => { + it("should include reasoning effort for mini models in Responses API format", async () => { const miniModelHandler = new XAIHandler({ apiModelId: "grok-3-mini", reasoningEffort: "high", @@ -269,7 +269,48 @@ describe("XAIHandler", () => { expect(mockResponsesCreate).toHaveBeenCalledWith( expect.objectContaining({ reasoning: expect.objectContaining({ - reasoning_effort: "high", + effort: "high", + }), + }), + ) + }) + + it("should include reasoning effort for grok-4.5 with default high effort", async () => { + const grok45Handler = new XAIHandler({ + apiModelId: "grok-4.5", + }) + + mockResponsesCreate.mockResolvedValueOnce(mockStream([])) + + const stream = grok45Handler.createMessage("test prompt", []) + await stream.next() + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "grok-4.5", + reasoning: expect.objectContaining({ + effort: "high", + }), + }), + ) + }) + + it("should include reasoning effort for grok-4.5 with custom low effort", async () => { + const grok45Handler = new XAIHandler({ + apiModelId: "grok-4.5", + reasoningEffort: "low", + }) + + mockResponsesCreate.mockResolvedValueOnce(mockStream([])) + + const stream = grok45Handler.createMessage("test prompt", []) + await stream.next() + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "grok-4.5", + reasoning: expect.objectContaining({ + effort: "low", }), }), ) diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 17ac40c8bf..15753c0043 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -120,9 +120,11 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler requestBody.parallel_tool_calls = metadata?.parallelToolCalls ?? true } - // Pass reasoning effort for models that support it (e.g., mini models) + // Pass reasoning effort for models that support it (e.g., grok-4.5, grok-3-mini). + // The xAI Responses API uses `reasoning: { effort }` format (not `reasoning_effort` + // which is the Chat Completions format), so we convert from the OpenAI params shape. if (model.reasoning) { - requestBody.reasoning = model.reasoning + requestBody.reasoning = { effort: model.reasoning.reasoning_effort } } let stream: AsyncIterable From 367013f31092258e57089505d537767e42980e3c Mon Sep 17 00:00:00 2001 From: Andrew Schmeder <149117631+awschmeder@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:09:30 -0700 Subject: [PATCH 31/89] feat(webview): surface condense button and context progress bar in collapsed task header (#680) * feat(webview): surface condense context button and progress bar in collapsed task header * feat(webview): replace FoldVertical with ListChevronsDownUp/ScissorsLineDashed, bump lucide-react to ^1.18.0 * fix(test): update SVG selector from lucide-fold-vertical to lucide-list-chevrons-down-up * feat(webview): surface condense context button and progress bar in collapsed task header * feat(webview): replace FoldVertical with ListChevronsDownUp/ScissorsLineDashed, bump lucide-react to ^1.18.0 * fix(test): update SVG selector from lucide-fold-vertical to lucide-list-chevrons-down-up * fix(webview): address PR review nits on collapsed task header - Remove .changeset entry (managed by maintainers, not agents) - Add stopPropagation guard to progress/condense container so clicking the progress bar cannot toggle the header on future refactors - Use ?? instead of || for maxTokens so a legitimate 0 is preserved - Assert condense click does not expand the header (regression guard) * fix(webview): use ?? for maxTokens in expanded task header --------- Co-authored-by: Elliott de Launay Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++--- webview-ui/package.json | 2 +- webview-ui/src/components/chat/TaskHeader.tsx | 16 ++++++++-- .../chat/__tests__/TaskHeader.spec.tsx | 32 ++++++++++--------- .../CondensationResultRow.tsx | 4 +-- .../TruncationResultRow.tsx | 4 +-- .../settings/ContextManagementSettings.tsx | 4 +-- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17de8c1b34..1aa321660e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -801,8 +801,8 @@ importers: specifier: ^11.1.0 version: 11.2.2 lucide-react: - specifier: ^0.577.0 - version: 0.577.0(react@18.3.1) + specifier: ^1.18.0 + version: 1.21.0(react@18.3.1) mermaid: specifier: ^11.4.1 version: 11.16.0 @@ -5956,8 +5956,8 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.577.0: - resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} + lucide-react@1.21.0: + resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -13943,7 +13943,7 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.577.0(react@18.3.1): + lucide-react@1.21.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/webview-ui/package.json b/webview-ui/package.json index db292e9bc5..83c1a84f90 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -50,7 +50,7 @@ "i18next": "^25.0.0", "katex": "^0.16.11", "lru-cache": "^11.1.0", - "lucide-react": "^0.577.0", + "lucide-react": "^1.18.0", "mermaid": "^11.4.1", "posthog-js": "^1.227.2", "pretty-bytes": "^7.0.0", diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 927d3d057d..8687c8e3fa 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -1,6 +1,6 @@ import { memo, useRef, useState, useMemo } from "react" import { useTranslation } from "react-i18next" -import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, FoldVertical, ArrowLeft } from "lucide-react" +import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, ListChevronsDownUp, ArrowLeft } from "lucide-react" import prettyBytes from "pretty-bytes" import type { ClineMessage } from "@roo-code/types" @@ -82,7 +82,7 @@ const TaskHeader = ({ const condenseButton = ( currentTaskItem && handleCondenseContext(currentTaskItem.id)} /> @@ -275,6 +275,16 @@ const TaskHeader = ({ )}
+
e.stopPropagation()}> + + {condenseButton} +
)} {/* Expanded state: Show task text and images */} @@ -315,7 +325,7 @@ const TaskHeader = ({ {condenseButton} diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 252cbbb722..ca7d8c8ce6 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -133,15 +133,22 @@ describe("TaskHeader", () => { expect(screen.queryByText(/\$/)).not.toBeInTheDocument() }) + it("should render the condense context button in the collapsed state", () => { + renderTaskHeader() + // Button is visible without expanding the task header + const buttons = screen.getAllByRole("button") + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-list-chevrons-down-up")) + expect(condenseButton).toBeDefined() + expect(condenseButton?.querySelector("svg")).toBeInTheDocument() + }) + it("should render the condense context button when expanded", () => { renderTaskHeader() - // First click to expand the task header const taskHeader = screen.getByText("Test task") fireEvent.click(taskHeader) - // Now find the condense button in the expanded state const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-list-chevrons-down-up")) expect(condenseButton).toBeDefined() expect(condenseButton?.querySelector("svg")).toBeInTheDocument() }) @@ -150,29 +157,24 @@ describe("TaskHeader", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ handleCondenseContext }) - // First click to expand the task header - const taskHeader = screen.getByText("Test task") - fireEvent.click(taskHeader) - - // Find the button that contains the FoldVertical icon + // Button is clickable in collapsed state without expanding first const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-list-chevrons-down-up")) expect(condenseButton).toBeDefined() fireEvent.click(condenseButton!) expect(handleCondenseContext).toHaveBeenCalledWith("test-task-id") + // Clicking the condense button must not expand the header (stopPropagation guard). + // The expanded state renders the "chat:task.title" label, which stays absent while collapsed. + expect(screen.queryByText("chat:task.title")).not.toBeInTheDocument() }) it("should disable the condense context button when buttonsDisabled is true", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ buttonsDisabled: true, handleCondenseContext }) - // First click to expand the task header - const taskHeader = screen.getByText("Test task") - fireEvent.click(taskHeader) - - // Find the button that contains the FoldVertical icon + // Button is disabled in collapsed state without expanding first const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-list-chevrons-down-up")) expect(condenseButton).toBeDefined() expect(condenseButton).toBeDisabled() fireEvent.click(condenseButton!) diff --git a/webview-ui/src/components/chat/context-management/CondensationResultRow.tsx b/webview-ui/src/components/chat/context-management/CondensationResultRow.tsx index 526ebebbe7..e9a9cfdf09 100644 --- a/webview-ui/src/components/chat/context-management/CondensationResultRow.tsx +++ b/webview-ui/src/components/chat/context-management/CondensationResultRow.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { useTranslation } from "react-i18next" import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react" -import { FoldVertical } from "lucide-react" +import { ListChevronsDownUp } from "lucide-react" import type { ContextCondense } from "@roo-code/types" @@ -32,7 +32,7 @@ export function CondensationResultRow({ data }: CondensationResultRowProps) { className="flex items-center justify-between cursor-pointer select-none" onClick={() => setIsExpanded(!isExpanded)}>
- + {t("chat:contextManagement.condensation.title")} diff --git a/webview-ui/src/components/chat/context-management/TruncationResultRow.tsx b/webview-ui/src/components/chat/context-management/TruncationResultRow.tsx index ec6a32f316..625c850161 100644 --- a/webview-ui/src/components/chat/context-management/TruncationResultRow.tsx +++ b/webview-ui/src/components/chat/context-management/TruncationResultRow.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { useTranslation } from "react-i18next" -import { FoldVertical } from "lucide-react" +import { ScissorsLineDashed } from "lucide-react" import type { ContextTruncation } from "@roo-code/types" @@ -33,7 +33,7 @@ export function TruncationResultRow({ data }: TruncationResultRowProps) { className="flex items-center justify-between cursor-pointer select-none" onClick={() => setIsExpanded(!isExpanded)}>
- + {t("chat:contextManagement.truncation.title")} diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index eaa566f36a..3127dde363 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -2,7 +2,7 @@ import { HTMLAttributes } from "react" import React from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" -import { FoldVertical } from "lucide-react" +import { ListChevronsDownUp } from "lucide-react" import { DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types" import { supportPrompt } from "@roo/support-prompt" @@ -516,7 +516,7 @@ export const ContextManagementSettings = ({ {autoCondenseContext && (
- +
{t("settings:contextManagement.condensingThreshold.label")}
From 3a4006a3293a3a9f15f7ca6ef092870456733836 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:41:29 -0400 Subject: [PATCH 32/89] chore: prepare v3.70.0 release (#930) --- CHANGELOG.md | 12 +++++ README.md | 52 +++++++-------------- locales/ca/README.md | 36 ++++++++------ locales/de/README.md | 34 ++++++++------ locales/es/README.md | 34 ++++++++------ locales/fr/README.md | 34 ++++++++------ locales/hi/README.md | 33 +++++++------ locales/id/README.md | 33 +++++++------ locales/it/README.md | 34 ++++++++------ locales/ja/README.md | 33 +++++++------ locales/ko/README.md | 32 +++++++------ locales/nl/README.md | 34 ++++++++------ locales/pl/README.md | 32 +++++++------ locales/pt-BR/README.md | 34 ++++++++------ locales/ru/README.md | 34 ++++++++------ locales/tr/README.md | 36 ++++++++------ locales/vi/README.md | 33 +++++++------ locales/zh-CN/README.md | 30 ++++++------ locales/zh-TW/README.md | 30 ++++++------ src/core/webview/ClineProvider.ts | 2 +- src/package.json | 2 +- webview-ui/src/i18n/locales/ca/chat.json | 6 +-- webview-ui/src/i18n/locales/de/chat.json | 6 +-- webview-ui/src/i18n/locales/en/chat.json | 6 +-- webview-ui/src/i18n/locales/es/chat.json | 6 +-- webview-ui/src/i18n/locales/fr/chat.json | 6 +-- webview-ui/src/i18n/locales/hi/chat.json | 6 +-- webview-ui/src/i18n/locales/id/chat.json | 6 +-- webview-ui/src/i18n/locales/it/chat.json | 6 +-- webview-ui/src/i18n/locales/ja/chat.json | 6 +-- webview-ui/src/i18n/locales/ko/chat.json | 6 +-- webview-ui/src/i18n/locales/nl/chat.json | 6 +-- webview-ui/src/i18n/locales/pl/chat.json | 6 +-- webview-ui/src/i18n/locales/pt-BR/chat.json | 6 +-- webview-ui/src/i18n/locales/ru/chat.json | 6 +-- webview-ui/src/i18n/locales/tr/chat.json | 6 +-- webview-ui/src/i18n/locales/vi/chat.json | 6 +-- webview-ui/src/i18n/locales/zh-CN/chat.json | 6 +-- webview-ui/src/i18n/locales/zh-TW/chat.json | 6 +-- 39 files changed, 411 insertions(+), 331 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0cb7f707..433b11aab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Zoo Code Changelog +## [3.70.0] + +### Minor Changes + +- Add Kenari as a first-class provider, an Indonesian OpenAI-compatible AI gateway billed in Rupiah covering Claude, GPT, DeepSeek, GLM, Kimi and more (#792 by @doedja, PR #793 by @doedja) +- Add OpenAI GPT-5.6 family support (sol, terra, luna) across both OpenAI Codex and OpenAI Native provider paths (#871 by @xRaTcHeT302, PR #876 by @navedmerchant) +- Add Grok 4.5 support and fix a latent xAI reasoning-effort format bug affecting Grok 4 Mini (#866 by @navedmerchant, PR #867 by @navedmerchant) +- Surface the context-compaction button and context window progress bar in the collapsed task header so context pressure can be monitored and acted on without expanding it (#606 by @awschmeder, PR #680 by @awschmeder) +- Fix(terminal): fix output loss and premature task completion on cold terminals by reading command output only after shell execution actually starts (#800 by @juneleung, PR #834 by @edelauna) +- Fix(zoo-gateway): enable image attach for Zoo Gateway and Vercel AI Gateway models based on live vision-capability tags instead of a static allowlist (PR #897 by @JamesRobert20) +- Chore(deps): routine dependency updates (PRs #807-#828 by @app/renovate) + ## [3.68.0] ### Minor Changes diff --git a/README.md b/README.md index ceffbc6d9a..5343674b7a 100644 --- a/README.md +++ b/README.md @@ -16,35 +16,17 @@ ## We are Zoo Code -> You may have seen the -> [recent announcement](https://x.com/mattrubens/status/2046636598859559114) -> from the Roo team 🦘🦘🦘. The TLDR is the team is winding down active Roo -> Code development as they focus on [Roomote](https://roomote.dev/). That news -> was difficult for many Roo users, this plugin means a lot to this community. +> Zoo Code continues development of this project after the Roo team wound down +> active Roo Code work to focus on [Roomote](https://roomote.dev/). Thank you +> to the Roo team for everything they built. > -> We want to thank the entire Roo team for the work they put into this plugin. -> We won't call out each person by name here, but we can all agree they are -> exceptional developers and, just as importantly, incredible people. Thank you -> to the Roo team. -> -> As Roo coders, we come in all kinds of shapes and sizes. Some of us are using -> it professionally in our day-to-day work, some are using it to tinker and -> scheme unimaginably complicated workflows. Some are using it to improve Roo -> itself while others are using it to improve the very models that Roo is using -> (super meta). The point we are making is that the community is -> diverse, and although a kangaroo 🦘🦘🦘 is a distinguished and noble animal, -> we felt a "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 of different species better reflected this -> diversity of the plugin's users. -> -> So we would like to announce that **Zoo Code** will continue development on -> this important project. The core team is a group of developers who contributed -> to Roo previously and care deeply about this plugin. We will continue to make -> model updates, fix bugs, and release features. But more than anything, we plan -> to listen to the community that made this plugin so special. Feel free to join -> us on [Discord](https://discord.gg/VxfP4Vx3gX), +> The core team is a group of developers who contributed to Roo previously and +> care deeply about this plugin. We will continue to make model updates, fix +> bugs, and release features, and we plan to listen closely to the community +> that made this plugin so special. Join us on +> [Discord](https://discord.gg/VxfP4Vx3gX), > [Reddit](https://www.reddit.com/r/ZooCode), or -> [open a PR or issue](https://github.com/Zoo-Code-Org/Zoo-Code), and above all, -> please stay involved, connected, and active as a community. +> [open a PR or issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -53,15 +35,15 @@ You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX) for this exact support, so if you are having problems or if you have question, jump on and ask. -## What's New in v3.68.0 +## What's New in v3.70.0 -- **Friendli provider with GLM-5.2 support** — use the latest GLM model through Friendli. -- **Native Ollama thinking/reasoning support** — preserve reasoning output end-to-end when you use Ollama models. -- **Anthropic custom `apiModelId` fix** — custom Anthropic model IDs now stay selected instead of silently falling back to `claude-sonnet-4-5`. -- Fix: Ollama provider tool result handling and premature context condensing. -- Fix: preserve the parent-child task link when a delegated subtask is interrupted. -- Improve Anthropic Vertex Claude content block handling for more reliable responses. -- CI, nightly publishing, and dependency/tooling updates. +- **OpenAI GPT-5.6 family** — `Sol`, `Terra`, and `Luna` are now available across both the OpenAI Codex and OpenAI Native provider paths. +- **Grok 4.5 support** — xAI's new flagship model, plus a reasoning-effort format fix that also benefits Grok 4 Mini. +- **Kenari provider support** — a first-class, OpenAI-compatible AI gateway billed in Rupiah covering Claude, GPT, DeepSeek, GLM, Kimi and more. +- Surface the context-compaction button and context window progress bar in the collapsed task header. +- Fix: terminal output loss and premature task completion on cold terminals. +- Fix: image attach for Zoo Gateway and Vercel AI Gateway models now follows live vision-capability tags instead of a static allowlist. +- Dependency and tooling updates.
🌐 Available languages diff --git a/locales/ca/README.md b/locales/ca/README.md index c58553fb68..1b113deb5e 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -16,29 +16,35 @@ ## Som Zoo Code -> Potser has vist el [recent anunci](https://x.com/mattrubens/status/2046636598859559114) de l'equip de Roo 🦘🦘🦘. El resum és que l'equip està reduint gradualment el desenvolupament actiu de Roo Code mentre es concentra en [Roomote](https://roomote.dev/). Aquesta notícia va ser difícil per a molts usuaris de Roo; aquest plugin significa molt per a aquesta comunitat. +> Zoo Code continua el desenvolupament d'aquest projecte després que l'equip de +> Roo aturés el desenvolupament actiu de Roo Code per centrar-se en +> [Roomote](https://roomote.dev/). Gràcies a l'equip de Roo per tot el que van +> construir. > -> Volem donar les gràcies a tot l'equip de Roo per la feina que han posat en aquest plugin. No destacarem cada persona pel seu nom aquí, però tots podem coincidir que són desenvolupadors excepcionals i, igual d'important, persones increïbles. Gràcies a l'equip de Roo. -> -> Els qui fem servir Roo venim de tota mena de perfils. Alguns l'utilitzen professionalment cada dia, altres el fan servir per imaginar fluxos de treball inimaginablement complexos. Alguns milloren Roo mateix, mentre que altres milloren els mateixos models que Roo utilitza (super meta). El que volem dir és que la comunitat és diversa i, tot i que un cangur 🦘🦘🦘 és un animal distingit i noble, vam sentir que un "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 reflectia millor aquesta diversitat d'usuaris del plugin. -> -> Per això volem anunciar que **Zoo Code** continuarà desenvolupant aquest projecte tan important. L'equip principal està format per desenvolupadors que ja havien contribuït a Roo i que valoren profundament aquest plugin. Continuarem actualitzant models, corregint errors i publicant funcionalitats. Però, per damunt de tot, pensem escoltar-vos a vosaltres, la comunitat, que heu fet aquest plugin tan especial. Així que uneix-te al nostre [Discord](https://discord.gg/VxfP4Vx3gX), al nostre [Reddit](https://www.reddit.com/r/ZooCode), o [crea un PR o una issue](https://github.com/Zoo-Code-Org/Zoo-Code); però, sobretot, et demanem que continuïs implicat, connectat i actiu com a comunitat. +> L'equip principal és un grup de desenvolupadors que ja havien contribuït a +> Roo anteriorment i que valoren profundament aquest plugin. Continuarem +> actualitzant models, corregint errors i publicant funcionalitats, i tenim +> intenció d'escoltar de prop la comunitat que ha fet aquest plugin tan +> especial. Uneix-te a nosaltres a +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), o +> [obre una PR o issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ ## Migració de Roo Code a Zoo Code -Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o alguna pregunta, entra i pregunta. +Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o algun dubte, entra i pregunta. -## Novetats a la v3.68.0 +## Novetats a la v3.70.0 -- **Proveïdor Friendli amb suport per a GLM-5.2** — utilitza el model GLM més recent a través de Friendli. -- **Suport natiu de thinking/reasoning per a Ollama** — conserva la sortida de raonament d'extrem a extrem quan fas servir models d'Ollama. -- **Correcció del `apiModelId` personalitzat d'Anthropic** — els identificadors de model personalitzats d'Anthropic ara es mantenen seleccionats en lloc de tornar en silenci a `claude-sonnet-4-5`. -- Correcció: gestió dels resultats d'eines del proveïdor Ollama i condensació prematura del context. -- Correcció: conserva l'enllaç pare-fill de la tasca quan s'interromp una subtasca delegada. -- Millora la gestió dels blocs de contingut de Claude a Anthropic Vertex per obtenir respostes més fiables. -- Actualitzacions de CI, publicació nightly i dependències/eines. +- **Família OpenAI GPT-5.6** — `Sol`, `Terra` i `Luna` ja estan disponibles tant a OpenAI Codex com a OpenAI Native. +- **Suport per a Grok 4.5** — el nou model insígnia de xAI, a més d'una correcció del format de l'esforç de raonament que també beneficia Grok 4 Mini. +- **Suport per al proveïdor Kenari** — una passarel·la d'IA de primer nivell, compatible amb OpenAI, facturada en rupies, que cobreix Claude, GPT, DeepSeek, GLM, Kimi i més. +- Mostra el botó de condensació de context i la barra de progrés de la finestra de context a la capçalera de tasca col·lapsada. +- Correcció: pèrdua de sortida del terminal i finalització prematura de tasques en terminals freds. +- Correcció: l'adjunció d'imatges per a Zoo Gateway i Vercel AI Gateway ara segueix les etiquetes de capacitat de visió en directe en lloc d'una llista blanca estàtica. +- Actualitzacions de dependències i eines. ## Què pot fer Zoo Code per TU? diff --git a/locales/de/README.md b/locales/de/README.md index 808b0faf3f..5a0d2950e8 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -16,13 +16,19 @@ ## Wir sind Zoo Code -> Du hast vielleicht die [neuliche Ankündigung](https://x.com/mattrubens/status/2046636598859559114) des Roo-Teams 🦘🦘🦘 gesehen. Die Kurzfassung ist, dass das Team die aktive Entwicklung von Roo Code zurückfährt, während es sich auf [Roomote](https://roomote.dev/) konzentriert. Diese Nachricht war für viele Roo-Nutzer schwer; dieses Plugin bedeutet dieser Community sehr viel. +> Zoo Code führt die Entwicklung dieses Projekts fort, nachdem das Roo-Team +> die aktive Arbeit an Roo Code eingestellt hat, um sich auf +> [Roomote](https://roomote.dev/) zu konzentrieren. Danke an das Roo-Team für +> alles, was sie aufgebaut haben. > -> Wir möchten dem gesamten Roo-Team für die Arbeit danken, die sie in dieses Plugin gesteckt haben. Wir nennen hier nicht jeden Einzelnen beim Namen, aber wir sind uns sicher alle einig, dass sie außergewöhnliche Entwickler und, genauso wichtig, unglaubliche Menschen sind. Danke an das Roo-Team. -> -> Roo-Coder gibt es in allen Formen und Größen. Manche nutzen es professionell im Arbeitsalltag, andere tüfteln damit an unvorstellbar komplexen Workflows. Einige verbessern Roo selbst, während andere die Modelle verbessern, die Roo verwendet (super meta). Der Punkt ist: Die Community ist vielfältig, und obwohl ein Känguru 🦘🦘🦘 ein edles Tier ist, fanden wir, dass ein „Zoo“ 🐘🦡🦒🦓🦛🦧🦭🦦 diese Vielfalt der Plugin-Nutzer besser widerspiegelt. -> -> Deshalb möchten wir ankündigen, dass **Zoo Code** die Entwicklung dieses wichtigen Projekts fortsetzen wird. Das Kernteam besteht aus Entwicklern, die zuvor zu Roo beigetragen haben und dieses Plugin sehr schätzen. Wir werden weiterhin Modelle aktualisieren, Fehler beheben und neue Funktionen veröffentlichen. Vor allem aber wollen wir auf euch hören, die Community, die dieses Plugin so besonders gemacht hat. Schließ dich also unserem [Discord](https://discord.gg/VxfP4Vx3gX), unserem [Reddit](https://www.reddit.com/r/ZooCode) an oder [eröffne einen PR oder ein Issue](https://github.com/Zoo-Code-Org/Zoo-Code) – vor allem bitten wir dich aber, als Community engagiert, vernetzt und aktiv zu bleiben. +> Das Kernteam besteht aus Entwicklern, die zuvor zu Roo beigetragen haben und +> dieses Plugin sehr schätzen. Wir werden weiterhin Modelle aktualisieren, +> Fehler beheben und neue Funktionen veröffentlichen, und wir haben vor, +> genau auf die Community zu hören, die dieses Plugin so besonders gemacht +> hat. Schließ dich uns an auf +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), oder +> [eröffne eine PR oder ein Issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Roo→Zoo-Migrationsleitfaden](https://docs.zoocode.dev/roo-to-zoo-migration). Wir wollen Nutzer beim Umstieg so gut wie möglich unterstützen, und genau dafür sind unser [Reddit](https://www.reddit.com/r/ZooCode) und [Discord](https://discord.gg/VxfP4Vx3gX) da. Wenn du Probleme hast oder Fragen auftauchen, komm vorbei und frag nach. -## Neu in v3.68.0 +## Neu in v3.70.0 -- **Friendli-Anbieter mit GLM-5.2-Unterstützung** — nutze das neueste GLM-Modell über Friendli. -- **Native Ollama-Unterstützung für thinking/reasoning** — behalte die Reasoning-Ausgabe durchgängig bei, wenn du Ollama-Modelle verwendest. -- **Fix für benutzerdefinierte `apiModelId` bei Anthropic** — benutzerdefinierte Anthropic-Modell-IDs bleiben jetzt ausgewählt, statt stillschweigend auf `claude-sonnet-4-5` zurückzufallen. -- Fix: Behandlung von Tool-Ergebnissen im Ollama-Anbieter und verfrühte Kontextkondensierung. -- Fix: Behalte die Eltern-Kind-Verknüpfung bei, wenn ein delegierter Untertask unterbrochen wird. -- Verbessere die Verarbeitung von Claude-Content-Blöcken in Anthropic Vertex für zuverlässigere Antworten. -- Updates für CI, Nightly-Publishing und Abhängigkeiten/Tooling. +- **OpenAI GPT-5.6-Familie** — `Sol`, `Terra` und `Luna` sind jetzt sowohl über OpenAI Codex als auch über OpenAI Native verfügbar. +- **Grok 4.5-Unterstützung** — xAIs neues Flaggschiff-Modell, plus ein Fix für das Reasoning-Effort-Format, von dem auch Grok 4 Mini profitiert. +- **Kenari-Anbieter-Unterstützung** — ein erstklassiges, OpenAI-kompatibles KI-Gateway mit Abrechnung in Rupiah, das Claude, GPT, DeepSeek, GLM, Kimi und mehr abdeckt. +- Zeige den Kontext-Kondensierungs-Button und den Fortschrittsbalken für das Kontextfenster im eingeklappten Task-Header an. +- Fix: Verlust von Terminal-Ausgaben und verfrühter Task-Abschluss bei kalten Terminals. +- Fix: Bild-Anhänge für Zoo Gateway und Vercel AI Gateway folgen jetzt den aktuellen Vision-Fähigkeits-Tags statt einer statischen Positivliste. +- Updates für Abhängigkeiten und Tooling. ## Was kann Zoo Code für DICH tun? diff --git a/locales/es/README.md b/locales/es/README.md index a63742ccaf..5015784888 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -16,13 +16,19 @@ ## Somos Zoo Code -> Puede que hayas visto el [anuncio reciente](https://x.com/mattrubens/status/2046636598859559114) del equipo de Roo 🦘🦘🦘. El resumen es que el equipo está reduciendo gradualmente el desarrollo activo de Roo Code mientras se centra en [Roomote](https://roomote.dev/). Esta noticia fue difícil para muchos usuarios de Roo; este plugin significa muchísimo para esta comunidad. +> Zoo Code continúa el desarrollo de este proyecto después de que el equipo +> de Roo detuviera el desarrollo activo de Roo Code para centrarse en +> [Roomote](https://roomote.dev/). Gracias al equipo de Roo por todo lo que +> construyeron. > -> Queremos agradecer a todo el equipo de Roo por el trabajo que dedicaron a este plugin. No vamos a mencionar a cada persona por su nombre aquí, pero todos podemos estar de acuerdo en que son desarrolladores excepcionales y, igual de importante, personas increíbles. Gracias al equipo de Roo. -> -> Quienes usamos Roo venimos de todo tipo de perfiles. Algunos lo usan profesionalmente en su día a día, otros lo usan para idear flujos de trabajo inimaginablemente complejos. Algunos lo usan para mejorar Roo, mientras que otros lo usan para mejorar los mismos modelos que Roo utiliza (super meta). El punto es que la comunidad es diversa y, aunque un canguro 🦘🦘🦘 es un animal distinguido y noble, sentimos que un "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 reflejaba mejor esa diversidad de usuarios del plugin. -> -> Así que queremos anunciar que **Zoo Code** continuará el desarrollo de este importante proyecto. El equipo principal está formado por desarrolladores que ya habían contribuido a Roo y se preocupan profundamente por este plugin. Seguiremos actualizando modelos, corrigiendo errores y lanzando funciones. Pero, más que nada, queremos escucharos a vosotros, la comunidad, que hizo este plugin tan especial. Así que no dudes en unirte a nuestro [Discord](https://discord.gg/VxfP4Vx3gX), a nuestro [Reddit](https://www.reddit.com/r/ZooCode), o [crear un PR o una issue](https://github.com/Zoo-Code-Org/Zoo-Code); pero, sobre todo, te pedimos que sigas involucrado, conectado y activo como comunidad. +> El equipo principal está formado por desarrolladores que ya habían +> contribuido a Roo y se preocupan profundamente por este plugin. Seguiremos +> actualizando modelos, corrigiendo errores y lanzando funciones, y tenemos +> previsto escuchar de cerca a la comunidad que hizo este plugin tan +> especial. Únete a nosotros en +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), o +> [abre un PR o issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guía de migración Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ayudar a los usuarios durante la transición, y para eso tenemos nuestro [Reddit](https://www.reddit.com/r/ZooCode) y [Discord](https://discord.gg/VxfP4Vx3gX). Si tienes problemas o alguna pregunta, entra y pregúntanos. -## Novedades de la v3.68.0 +## Novedades de la v3.70.0 -- **Proveedor Friendli con soporte para GLM-5.2** — usa el modelo GLM más reciente a través de Friendli. -- **Compatibilidad nativa de thinking/reasoning para Ollama** — conserva la salida de razonamiento de extremo a extremo cuando uses modelos de Ollama. -- **Corrección de `apiModelId` personalizado de Anthropic** — los IDs de modelo personalizados de Anthropic ahora permanecen seleccionados en lugar de volver silenciosamente a `claude-sonnet-4-5`. -- Corrección: manejo de resultados de herramientas del proveedor Ollama y condensación prematura del contexto. -- Corrección: conserva el vínculo padre-hijo de la tarea cuando se interrumpe una subtarea delegada. -- Mejora el manejo de bloques de contenido de Claude en Anthropic Vertex para obtener respuestas más fiables. -- Actualizaciones de CI, publicación nightly y dependencias/herramientas. +- **Familia OpenAI GPT-5.6** — `Sol`, `Terra` y `Luna` ya están disponibles tanto en OpenAI Codex como en OpenAI Native. +- **Soporte para Grok 4.5** — el nuevo modelo insignia de xAI, además de una corrección del formato de esfuerzo de razonamiento que también beneficia a Grok 4 Mini. +- **Soporte para el proveedor Kenari** — una pasarela de IA de primer nivel, compatible con OpenAI, facturada en rupias, que cubre Claude, GPT, DeepSeek, GLM, Kimi y más. +- Muestra el botón de condensación de contexto y la barra de progreso de la ventana de contexto en el encabezado de tarea colapsado. +- Corrección: pérdida de salida de terminal y finalización prematura de tareas en terminales fríos. +- Corrección: la adjunción de imágenes para Zoo Gateway y Vercel AI Gateway ahora sigue las etiquetas de capacidad de visión en vivo en lugar de una lista blanca estática. +- Actualizaciones de dependencias y herramientas. ## ¿Qué puede hacer Zoo Code por TI? diff --git a/locales/fr/README.md b/locales/fr/README.md index a164e50050..cbc318e898 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -16,13 +16,19 @@ ## Nous sommes Zoo Code -> Tu as peut-être vu la [récente annonce](https://x.com/mattrubens/status/2046636598859559114) de l'équipe Roo 🦘🦘🦘. En résumé, l'équipe réduit progressivement le développement actif de Roo Code pendant qu'elle se concentre sur [Roomote](https://roomote.dev/). La nouvelle a été difficile pour beaucoup d'utilisateurs de Roo ; cette extension compte énormément pour cette communauté. +> Zoo Code poursuit le développement de ce projet après que l'équipe Roo a +> arrêté le développement actif de Roo Code pour se concentrer sur +> [Roomote](https://roomote.dev/). Merci à l'équipe Roo pour tout ce qu'elle +> a construit. > -> Nous voulons remercier toute l'équipe Roo pour le travail qu'elle a consacré à cette extension. Nous n'allons pas citer chaque personne ici, mais on peut tous s'accorder à dire que ce sont des développeurs exceptionnels et, tout aussi important, des personnes incroyables. Merci à l'équipe Roo. -> -> Les utilisateurs de Roo viennent d'horizons très différents. Certains l'utilisent au quotidien dans un cadre professionnel, d'autres s'en servent pour imaginer des workflows incroyablement complexes. Certains l'utilisent pour améliorer Roo lui-même, tandis que d'autres l'utilisent pour améliorer les modèles mêmes que Roo exploite (super méta). En bref, la communauté est diverse, et même si un kangourou 🦘🦘🦘 est un animal noble et distingué, nous avons trouvé qu'un « Zoo » 🐘🦡🦒🦓🦛🦧🦭🦦 reflétait mieux cette diversité des utilisateurs de l'extension. -> -> Nous voulons donc annoncer que **Zoo Code** poursuivra le développement de cet important projet. L'équipe principale est composée de développeurs qui ont déjà contribué à Roo et qui tiennent profondément à cette extension. Nous continuerons à mettre à jour les modèles, corriger les bugs et livrer de nouvelles fonctionnalités. Mais surtout, nous voulons vous écouter, vous, la communauté, qui avez rendu cette extension si spéciale. N'hésite donc pas à rejoindre notre [Discord](https://discord.gg/VxfP4Vx3gX), notre [Reddit](https://www.reddit.com/r/ZooCode), ou à [ouvrir une PR ou une issue](https://github.com/Zoo-Code-Org/Zoo-Code) ; plus que tout, nous te demandons de rester impliqué, connecté et actif au sein de la communauté. +> L'équipe principale est composée de développeurs qui ont déjà contribué à +> Roo et qui tiennent profondément à cette extension. Nous continuerons à +> mettre à jour les modèles, corriger les bugs et livrer de nouvelles +> fonctionnalités, et nous comptons bien écouter attentivement la communauté +> qui a rendu cette extension si spéciale. Rejoins-nous sur +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), ou +> [ouvre une PR ou une issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [guide de migration Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). On veut aider au maximum pendant la transition, et notre [Reddit](https://www.reddit.com/r/ZooCode) et notre [Discord](https://discord.gg/VxfP4Vx3gX) sont là pour ça. Si tu rencontres un problème ou si tu as une question, viens demander. -## Nouveautés de la v3.68.0 +## Nouveautés de la v3.70.0 -- **Provider Friendli avec prise en charge de GLM-5.2** — utilise le dernier modèle GLM via Friendli. -- **Prise en charge native du thinking/reasoning pour Ollama** — préserve la sortie de raisonnement de bout en bout quand tu utilises des modèles Ollama. -- **Correction du `apiModelId` personnalisé d'Anthropic** — les identifiants de modèle Anthropic personnalisés restent désormais sélectionnés au lieu de revenir silencieusement à `claude-sonnet-4-5`. -- Correctif : gestion des résultats d'outils du fournisseur Ollama et condensation prématurée du contexte. -- Correctif : préserve le lien parent-enfant de la tâche lorsqu'une sous-tâche déléguée est interrompue. -- Améliore la gestion des blocs de contenu Claude dans Anthropic Vertex pour des réponses plus fiables. -- Mises à jour de la CI, de la publication nightly et des dépendances/outils. +- **Famille OpenAI GPT-5.6** — `Sol`, `Terra` et `Luna` sont désormais disponibles à la fois via OpenAI Codex et OpenAI Native. +- **Prise en charge de Grok 4.5** — le nouveau modèle phare de xAI, ainsi qu'une correction du format de l'effort de raisonnement qui profite aussi à Grok 4 Mini. +- **Prise en charge du fournisseur Kenari** — une passerelle IA de premier ordre, compatible OpenAI, facturée en roupies, couvrant Claude, GPT, DeepSeek, GLM, Kimi et plus encore. +- Affiche le bouton de condensation du contexte et la barre de progression de la fenêtre de contexte dans l'en-tête de tâche réduit. +- Correctif : perte de sortie du terminal et fin de tâche prématurée sur des terminaux froids. +- Correctif : l'ajout d'images pour Zoo Gateway et Vercel AI Gateway suit désormais les balises de capacité de vision en direct au lieu d'une liste blanche statique. +- Mises à jour des dépendances et des outils. ## Que peut faire Zoo Code pour VOUS ? diff --git a/locales/hi/README.md b/locales/hi/README.md index a9ccd13cc3..745b5d7540 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -16,13 +16,18 @@ ## हम हैं Zoo Code -> आपने शायद Roo टीम 🦘🦘🦘 की [हाल की घोषणा](https://x.com/mattrubens/status/2046636598859559114) देखी होगी। TLDR यह है कि टीम [Roomote](https://roomote.dev/) पर focus करते हुए Roo Code का active development धीरे-धीरे बंद कर रही है। यह खबर बहुत से Roo users के लिए मुश्किल थी; यह plugin इस community के लिए बहुत मायने रखता है। +> Roo टीम के [Roomote](https://roomote.dev/) पर focus करने के लिए Roo Code का +> active development बंद करने के बाद, Zoo Code इस project का development +> आगे बढ़ा रहा है। जो कुछ भी उन्होंने बनाया, उसके लिए Roo टीम का धन्यवाद। > -> हम पूरे Roo टीम को इस plugin पर लगाए गए उनके काम के लिए धन्यवाद देना चाहते हैं। हम यहाँ हर व्यक्ति का नाम अलग-अलग नहीं लेंगे, लेकिन हम सब इस बात से सहमत हो सकते हैं कि वे exceptional developers हैं और, उतना ही ज़रूरी, incredible लोग भी हैं। Roo टीम को धन्यवाद। -> -> Roo users हर तरह के हैं। कुछ लोग इसे अपने day-to-day professional काम में इस्तेमाल करते हैं, कुछ लोग इससे बेहद जटिल workflows बनाते हैं। कुछ लोग खुद Roo को बेहतर बनाते हैं, जबकि कुछ वही models बेहतर बनाते हैं जिन्हें Roo इस्तेमाल करता है (super meta)। हमारी बात बस इतनी है कि community बहुत diverse है, और जबकि kangaroo 🦘🦘🦘 एक शानदार और सम्मानित जानवर है, हमें लगा कि अलग-अलग species वाला "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 plugin users की इस diversity को बेहतर दिखाता है। -> -> इसलिए हम यह घोषणा करना चाहते हैं कि **Zoo Code** इस महत्वपूर्ण project का development जारी रखेगा। Core team ऐसे developers का समूह है जिन्होंने पहले Roo में योगदान दिया है और इस plugin की गहराई से परवाह करते हैं। हम models update करते रहेंगे, bugs fix करते रहेंगे और features release करते रहेंगे। लेकिन सबसे बढ़कर, हम आप community की बात सुनना चाहते हैं, जिसने इस plugin को इतना खास बनाया। इसलिए बेझिझक हमारे [Discord](https://discord.gg/VxfP4Vx3gX), हमारे [Reddit](https://www.reddit.com/r/ZooCode) से जुड़ें, या [PR या issue बनाएं](https://github.com/Zoo-Code-Org/Zoo-Code); लेकिन सबसे ज़्यादा हम आपसे यही कहेंगे कि community के रूप में जुड़े रहें, सक्रिय रहें और शामिल रहें। +> Core team ऐसे developers का समूह है जिन्होंने पहले Roo में योगदान दिया है +> और इस plugin की गहराई से परवाह करते हैं। हम models update करते रहेंगे, +> bugs fix करते रहेंगे और features release करते रहेंगे, और हम उस community +> की बात ध्यान से सुनने की योजना बना रहे हैं जिसने इस plugin को इतना खास +> बनाया। हमारे साथ जुड़ो +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), या +> [PR या issue खोलें](https://github.com/Zoo-Code-Org/Zoo-Code) पर। > > _-Zoo Code Team_ @@ -30,15 +35,15 @@ Roo Code से Zoo Code में आने के लिए एक quick guide तुम्हें [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration) में मिल जाएगी। We plan to help users as much as possible during the transition, और उसी support के लिए हमारा [Reddit](https://www.reddit.com/r/ZooCode) और [Discord](https://discord.gg/VxfP4Vx3gX) है। अगर तुम्हें कोई problem हो या कोई question हो, आकर पूछो। -## v3.68.0 में नया क्या है +## v3.70.0 में नया क्या है -- **GLM-5.2 समर्थन के साथ Friendli प्रदाता** — Friendli के जरिए नवीनतम GLM मॉडल का उपयोग करें। -- **Ollama के लिए नेटिव thinking/reasoning समर्थन** — Ollama मॉडल इस्तेमाल करते समय reasoning आउटपुट को शुरू से अंत तक सुरक्षित रखें। -- **Anthropic कस्टम `apiModelId` फिक्स** — कस्टम Anthropic मॉडल ID अब चुपचाप `claude-sonnet-4-5` पर वापस जाने के बजाय चुनी हुई रहती हैं। -- फिक्स: Ollama प्रदाता में tool result हैंडलिंग और समय से पहले context condensing। -- फिक्स: delegated subtask रुकने पर parent-child task link को बनाए रखें। -- अधिक विश्वसनीय responses के लिए Anthropic Vertex Claude content block handling में सुधार। -- CI, nightly publishing, और dependency/tooling अपडेट्स। +- **OpenAI GPT-5.6 family** — `Sol`, `Terra`, और `Luna` अब OpenAI Codex और OpenAI Native दोनों provider paths पर उपलब्ध हैं। +- **Grok 4.5 समर्थन** — xAI का नया flagship model, साथ ही एक reasoning-effort format fix जिसका फायदा Grok 4 Mini को भी मिलता है। +- **Kenari provider समर्थन** — एक first-class, OpenAI-compatible AI gateway जो Rupiah में बिल होता है और Claude, GPT, DeepSeek, GLM, Kimi और अधिक को cover करता है। +- Collapsed task header में context-compaction button और context window progress bar दिखाएं। +- फिक्स: cold terminals पर terminal output loss और समय से पहले task completion। +- फिक्स: Zoo Gateway और Vercel AI Gateway models के लिए image attach अब static allowlist के बजाय live vision-capability tags को follow करता है। +- Dependency और tooling अपडेट्स। ## Zoo Code आपके लिए क्या कर सकता है? diff --git a/locales/id/README.md b/locales/id/README.md index 0e2d652969..a2f995688d 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -16,13 +16,18 @@ ## Kami adalah Zoo Code -> Kamu mungkin sudah melihat [pengumuman terbaru](https://x.com/mattrubens/status/2046636598859559114) dari tim Roo 🦘🦘🦘. Singkatnya, tim sedang mengurangi pengembangan aktif Roo Code secara bertahap sambil memusatkan perhatian pada [Roomote](https://roomote.dev/). Kabar ini berat bagi banyak pengguna Roo; plugin ini sangat berarti bagi komunitas ini. +> Zoo Code melanjutkan pengembangan proyek ini setelah tim Roo menghentikan +> pengembangan aktif Roo Code untuk fokus pada [Roomote](https://roomote.dev/). +> Terima kasih kepada tim Roo atas semua yang telah mereka bangun. > -> Kami ingin berterima kasih kepada seluruh tim Roo atas kerja yang mereka curahkan ke plugin ini. Kami tidak akan menyebut satu per satu nama mereka di sini, tetapi kita semua bisa sepakat bahwa mereka adalah developer yang luar biasa dan, sama pentingnya, orang-orang yang hebat. Terima kasih untuk tim Roo. -> -> Pengguna Roo datang dari berbagai macam latar belakang. Ada yang memakainya secara profesional dalam pekerjaan sehari-hari, ada juga yang memakainya untuk merancang workflow yang sangat rumit. Sebagian memakainya untuk meningkatkan Roo itu sendiri, sementara yang lain memakainya untuk meningkatkan model yang digunakan Roo (super meta). Intinya, komunitas ini beragam, dan meskipun kanguru 🦘🦘🦘 adalah hewan yang gagah dan mulia, kami merasa "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 lebih mencerminkan keberagaman pengguna plugin ini. -> -> Jadi kami ingin mengumumkan bahwa **Zoo Code** akan melanjutkan pengembangan proyek penting ini. Tim inti kami terdiri dari para developer yang sebelumnya pernah berkontribusi ke Roo dan sangat peduli pada plugin ini. Kami akan terus menghadirkan pembaruan model, memperbaiki bug, dan merilis fitur. Namun lebih dari segalanya, kami ingin mendengarkan kamu, komunitas yang membuat plugin ini begitu istimewa. Jadi silakan bergabung ke [Discord](https://discord.gg/VxfP4Vx3gX) kami, [Reddit](https://www.reddit.com/r/ZooCode) kami, atau [buat PR atau issue](https://github.com/Zoo-Code-Org/Zoo-Code); tetapi yang paling penting, kami meminta kamu untuk tetap terlibat, terhubung, dan aktif sebagai komunitas. +> Tim inti kami terdiri dari para developer yang sebelumnya pernah +> berkontribusi ke Roo dan sangat peduli pada plugin ini. Kami akan terus +> menghadirkan pembaruan model, memperbaiki bug, dan merilis fitur, dan kami +> berencana untuk mendengarkan dengan saksama komunitas yang membuat plugin +> ini begitu istimewa. Gabung bersama kami di +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), atau +> [buka PR atau issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +35,15 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di [panduan migrasi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Kami ingin membantu pengguna semaksimal mungkin selama masa transisi, dan itulah gunanya [Reddit](https://www.reddit.com/r/ZooCode) dan [Discord](https://discord.gg/VxfP4Vx3gX) kami. Kalau kamu mengalami masalah atau punya pertanyaan, langsung mampir dan tanya. -## Yang Baru di v3.68.0 +## Yang Baru di v3.70.0 -- **Provider Friendli dengan dukungan GLM-5.2** — gunakan model GLM terbaru lewat Friendli. -- **Dukungan thinking/reasoning native untuk Ollama** — pertahankan keluaran penalaran dari awal hingga akhir saat kamu memakai model Ollama. -- **Perbaikan `apiModelId` kustom Anthropic** — ID model Anthropic kustom kini tetap terpilih alih-alih diam-diam kembali ke `claude-sonnet-4-5`. -- Perbaikan: penanganan hasil tool penyedia Ollama dan pemadatan konteks yang terlalu dini. -- Perbaikan: pertahankan tautan induk-anak tugas saat subtugas yang didelegasikan terputus. -- Peningkatan penanganan blok konten Claude Anthropic Vertex untuk respons yang lebih andal. -- Pembaruan CI, publikasi nightly, dan dependensi/tooling. +- **Keluarga OpenAI GPT-5.6** — `Sol`, `Terra`, dan `Luna` kini tersedia lewat jalur provider OpenAI Codex maupun OpenAI Native. +- **Dukungan Grok 4.5** — model unggulan baru dari xAI, plus perbaikan format reasoning-effort yang juga bermanfaat untuk Grok 4 Mini. +- **Dukungan provider Kenari** — AI gateway kelas satu yang kompatibel dengan OpenAI, ditagih dalam Rupiah, mencakup Claude, GPT, DeepSeek, GLM, Kimi, dan lainnya. +- Menampilkan tombol pemadatan konteks dan progress bar jendela konteks di header tugas yang diciutkan. +- Perbaikan: hilangnya output terminal dan penyelesaian tugas prematur pada terminal yang dingin (cold terminal). +- Perbaikan: attach gambar untuk model Zoo Gateway dan Vercel AI Gateway kini mengikuti tag kemampuan vision secara langsung, bukan allowlist statis. +- Pembaruan dependensi dan tooling. ## Apa yang Bisa Zoo Code Lakukan Untuk ANDA? diff --git a/locales/it/README.md b/locales/it/README.md index 6f47b7d0d6..7196b90812 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -16,13 +16,19 @@ ## Siamo Zoo Code -> Potresti aver visto il [recente annuncio](https://x.com/mattrubens/status/2046636598859559114) del team di Roo 🦘🦘🦘. In breve, il team sta riducendo progressivamente lo sviluppo attivo di Roo Code mentre si concentra su [Roomote](https://roomote.dev/). Questa notizia è stata difficile per molti utenti di Roo; questo plugin significa tantissimo per questa comunità. +> Zoo Code continua lo sviluppo di questo progetto dopo che il team di Roo +> ha interrotto lo sviluppo attivo di Roo Code per concentrarsi su +> [Roomote](https://roomote.dev/). Grazie al team di Roo per tutto quello +> che hanno costruito. > -> Vogliamo ringraziare tutto il team di Roo per il lavoro che ha dedicato a questo plugin. Non faremo il nome di ogni singola persona qui, ma possiamo tutti concordare sul fatto che siano sviluppatori eccezionali e, cosa altrettanto importante, persone incredibili. Grazie al team di Roo. -> -> Gli utenti di Roo arrivano da ogni tipo di contesto. Alcuni lo usano professionalmente ogni giorno, altri lo usano per ideare workflow incredibilmente complessi. Alcuni lo usano per migliorare Roo stesso, mentre altri lo usano per migliorare i modelli che Roo utilizza (super meta). Il punto è che la community è eterogenea e, anche se un canguro 🦘🦘🦘 è un animale distinto e nobile, ci è sembrato che uno "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 rappresentasse meglio questa diversità di utenti del plugin. -> -> Per questo vogliamo annunciare che **Zoo Code** continuerà lo sviluppo di questo importante progetto. Il team principale è composto da sviluppatori che avevano già contribuito a Roo e che tengono profondamente a questo plugin. Continueremo ad aggiornare i modelli, correggere bug e pubblicare funzionalità. Ma soprattutto, vogliamo ascoltare voi, la community che ha reso questo plugin così speciale. Quindi sentiti libero di unirti al nostro [Discord](https://discord.gg/VxfP4Vx3gX), al nostro [Reddit](https://www.reddit.com/r/ZooCode), o di [aprire una PR o una issue](https://github.com/Zoo-Code-Org/Zoo-Code); ma più di ogni altra cosa, ti chiediamo di restare coinvolto, connesso e attivo come community. +> Il team principale è un gruppo di sviluppatori che avevano già contribuito +> a Roo e che tengono profondamente a questo plugin. Continueremo ad +> aggiornare i modelli, correggere bug e pubblicare funzionalità, e +> intendiamo ascoltare con attenzione la community che ha reso questo +> plugin così speciale. Unisciti a noi su +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), oppure +> [apri una PR o una issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida alla migrazione Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Vogliamo aiutare gli utenti il più possibile durante la transizione, e per questo abbiamo il nostro [Reddit](https://www.reddit.com/r/ZooCode) e il nostro [Discord](https://discord.gg/VxfP4Vx3gX). Se hai problemi o domande, passa pure e chiedi. -## Novità in v3.68.0 +## Novità in v3.70.0 -- **Provider Friendli con supporto GLM-5.2** — usa il modello GLM più recente tramite Friendli. -- **Supporto nativo thinking/reasoning per Ollama** — conserva l'output di ragionamento end-to-end quando usi i modelli Ollama. -- **Correzione del `apiModelId` personalizzato di Anthropic** — gli ID modello Anthropic personalizzati ora restano selezionati invece di tornare silenziosamente a `claude-sonnet-4-5`. -- Correzione: gestione dei risultati degli strumenti del provider Ollama e condensazione prematura del contesto. -- Correzione: mantieni il collegamento padre-figlio dell'attività quando una sottoattività delegata viene interrotta. -- Migliora la gestione dei blocchi di contenuto Claude in Anthropic Vertex per risposte più affidabili. -- Aggiornamenti a CI, pubblicazione nightly e dipendenze/tooling. +- **Famiglia OpenAI GPT-5.6** — `Sol`, `Terra` e `Luna` sono ora disponibili sia tramite OpenAI Codex sia tramite OpenAI Native. +- **Supporto Grok 4.5** — il nuovo modello di punta di xAI, oltre a una correzione del formato reasoning-effort che avvantaggia anche Grok 4 Mini. +- **Supporto provider Kenari** — un AI gateway di prim'ordine, compatibile con OpenAI, fatturato in Rupie, che copre Claude, GPT, DeepSeek, GLM, Kimi e altro ancora. +- Mostra il pulsante di condensazione del contesto e la barra di avanzamento della finestra di contesto nell'intestazione dell'attività compressa. +- Correzione: perdita di output del terminale e completamento prematuro delle attività su terminali freddi. +- Correzione: l'allegato di immagini per i modelli Zoo Gateway e Vercel AI Gateway ora segue i tag di capacità di visione in tempo reale invece di una allowlist statica. +- Aggiornamenti di dipendenze e tooling. ## Cosa può fare Zoo Code per TE? diff --git a/locales/ja/README.md b/locales/ja/README.md index 0dfe4ecbd5..47ccf716d0 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -16,13 +16,18 @@ ## 私たちは Zoo Code です -> Roo チーム 🦘🦘🦘 の[最近の発表](https://x.com/mattrubens/status/2046636598859559114)を見たかもしれません。要するに、チームは [Roomote](https://roomote.dev/) に注力するため、Roo Code の積極的な開発を段階的に縮小しています。この知らせは多くの Roo ユーザーにとってつらいものでした。このプラグインはこのコミュニティにとってとても大切な存在です。 +> Roo チームが [Roomote](https://roomote.dev/) に注力するため Roo Code の +> 積極的な開発を終了した後、Zoo Code がこのプロジェクトの開発を継続してい +> ます。彼らが築き上げてきたすべてに、Roo チームへ感謝します。 > -> このプラグインに注いでくれた努力に対して、Roo チーム全員に感謝したいと思います。ここで一人ひとりの名前を挙げることはしませんが、彼らが優れた開発者であり、同じくらい素晴らしい人たちであることに、みんなが同意できるはずです。Roo チームに感謝します。 -> -> Roo ユーザーには本当にいろいろな人がいます。日々の仕事でプロとして使っている人もいれば、信じられないほど複雑なワークフローを考えるために使っている人もいます。Roo 自体を改善するために使う人もいれば、Roo が使っているモデルそのものを改善するために使う人もいます(超メタです)。言いたいのは、コミュニティは多様だということです。カンガルー 🦘🦘🦘 も立派で高貴な動物ですが、さまざまな種が集まる「Zoo」🐘🦡🦒🦓🦛🦧🦭🦦 のほうが、このプラグインのユーザーの多様性をよりよく表していると感じました。 -> -> そこで、**Zoo Code** がこの重要なプロジェクトの開発を継続することをお知らせします。コアチームは、以前 Roo に貢献していた開発者たちで構成されており、このプラグインを心から大切に思っています。これからもモデルの更新、バグ修正、機能リリースを続けていきます。ですが何よりも、このプラグインを特別なものにしてくれたコミュニティのみなさんの声に耳を傾けたいと考えています。ぜひ私たちの [Discord](https://discord.gg/VxfP4Vx3gX)、[Reddit](https://www.reddit.com/r/ZooCode) に参加したり、[PR や issue を作成](https://github.com/Zoo-Code-Org/Zoo-Code)したりしてください。何よりも、コミュニティとして関わり続け、つながり続け、活発でいてほしいと願っています。 +> コアチームは、以前 Roo に貢献していた開発者たちで構成されており、この +> プラグインを心から大切に思っています。これからもモデルの更新、バグ修 +> 正、機能リリースを続けていき、このプラグインを特別なものにしてくれた +> コミュニティの声に耳を傾けていくつもりです。ぜひ私たちの +> [Discord](https://discord.gg/VxfP4Vx3gX)、 +> [Reddit](https://www.reddit.com/r/ZooCode) に参加したり、 +> [PR や issue を作成](https://github.com/Zoo-Code-Org/Zoo-Code)したりして +> ください。 > > _-Zoo Code Team_ @@ -30,15 +35,15 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo→Zoo 移行ガイド](https://docs.zoocode.dev/roo-to-zoo-migration) で確認できます。移行中のユーザーをできるだけ支援したいと考えていて、そのために [Reddit](https://www.reddit.com/r/ZooCode) と [Discord](https://discord.gg/VxfP4Vx3gX) を用意しています。困ったことや質問があれば、気軽に参加して聞いてください。 -## v3.68.0 の新機能 +## v3.70.0 の新機能 -- **GLM-5.2 をサポートした Friendli プロバイダー** — Friendli 経由で最新の GLM モデルを使えます。 -- **Ollama のネイティブ thinking/reasoning サポート** — Ollama モデル使用時に reasoning 出力をエンドツーエンドで保持します。 -- **Anthropic のカスタム `apiModelId` 修正** — カスタム Anthropic モデル ID が `claude-sonnet-4-5` に黙ってフォールバックせず、そのまま選択された状態を維持します。 -- 修正: Ollama プロバイダーのツール結果処理と早すぎるコンテキスト圧縮。 -- 修正: 委譲されたサブタスクが中断されたときに親子タスクのリンクを維持。 -- Anthropic Vertex での Claude コンテンツブロック処理を改善し、応答の信頼性を向上。 -- CI、nightly 公開、依存関係/ツール更新。 +- **OpenAI GPT-5.6 ファミリー** — `Sol`、`Terra`、`Luna` が OpenAI Codex と OpenAI Native の両方のプロバイダーパスで利用可能になりました。 +- **Grok 4.5 サポート** — xAI の新しいフラッグシップモデルに加え、Grok 4 Mini にも恩恵のある reasoning-effort フォーマット修正。 +- **Kenari プロバイダーサポート** — Claude、GPT、DeepSeek、GLM、Kimi などをカバーする、ルピア建てのファーストクラスの OpenAI 互換 AI ゲートウェイ。 +- 折りたたまれたタスクヘッダーにコンテキスト圧縮ボタンとコンテキストウィンドウの進捗バーを表示。 +- 修正: コールドターミナルでのターミナル出力の消失と早すぎるタスク完了。 +- 修正: Zoo Gateway と Vercel AI Gateway モデルの画像添付が、静的な許可リストではなくライブの vision 対応タグに従うように。 +- 依存関係とツールの更新。 ## Zoo Codeがあなたのためにできること diff --git a/locales/ko/README.md b/locales/ko/README.md index 33b19fcfc8..a5c41b6863 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -16,13 +16,17 @@ ## 우리는 Zoo Code입니다 -> Roo 팀 🦘🦘🦘의 [최근 발표](https://x.com/mattrubens/status/2046636598859559114)를 봤을지도 몰라. 한줄 요약하면, 팀은 [Roomote](https://roomote.dev/)에 집중하면서 Roo Code의 적극적인 개발을 점차 줄여 나가고 있어. 이 소식은 많은 Roo 사용자들에게 힘들었고, 이 플러그인은 이 커뮤니티에 큰 의미가 있어. +> Roo 팀이 [Roomote](https://roomote.dev/)에 집중하기 위해 Roo Code의 적극적인 +> 개발을 중단한 뒤, Zoo Code가 이 프로젝트의 개발을 이어가고 있어. 그동안 +> 쌓아 온 모든 것에 대해 Roo 팀에게 고마워. > -> 이 플러그인에 쏟아 준 Roo 팀 전체의 노력에 감사하고 싶어. 여기서 한 사람씩 이름을 부르지는 않겠지만, 모두가 그들이 뛰어난 개발자일 뿐 아니라 정말 멋진 사람들이라는 데 동의할 거야. Roo 팀에게 고마워. -> -> Roo 사용자들은 정말 다양합니다. 어떤 사람은 일상적인 업무에서 전문적으로 사용하고, 어떤 사람은 상상하기 어려울 정도로 복잡한 워크플로를 구상하는 데 사용합니다. 어떤 사람은 Roo 자체를 개선하는 데 쓰고, 또 어떤 사람은 Roo가 사용하는 모델 자체를 개선하는 데 씁니다 (정말 메타하죠). 우리가 말하고 싶은 핵심은 커뮤니티가 다양하다는 것입니다. 캥거루 🦘🦘🦘도 충분히 멋지고 고귀한 동물이지만, 여러 종이 함께 있는 "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦가 이 플러그인 사용자의 다양성을 더 잘 보여준다고 느꼈습니다. -> -> 그래서 **Zoo Code**가 이 중요한 프로젝트의 개발을 이어가겠다고 발표하고자 합니다. 핵심 팀은 이전에 Roo에 기여했고 이 플러그인을 깊이 아끼는 개발자들로 구성되어 있습니다. 우리는 계속해서 모델을 업데이트하고, 버그를 수정하고, 기능을 출시할 것입니다. 하지만 무엇보다도 이 플러그인을 특별하게 만들어 준 커뮤니티 여러분의 목소리에 귀 기울이려 합니다. 그러니 우리의 [Discord](https://discord.gg/VxfP4Vx3gX), [Reddit](https://www.reddit.com/r/ZooCode)에 참여하거나 [PR 또는 issue 를 만들어](https://github.com/Zoo-Code-Org/Zoo-Code) 주세요. 무엇보다도 커뮤니티로서 계속 연결되고, 참여하고, 활발하게 함께해 주시길 부탁드립니다. +> 핵심 팀은 이전에 Roo에 기여했고 이 플러그인을 깊이 아끼는 개발자들로 +> 구성되어 있어. 우리는 계속해서 모델을 업데이트하고, 버그를 수정하고, +> 기능을 출시할 거고, 이 플러그인을 특별하게 만들어 준 커뮤니티의 목소리에 +> 귀 기울일 계획이야. 우리와 함께해 +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), 또는 +> [PR이나 issue 열기](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +34,15 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그레이션 가이드](https://docs.zoocode.dev/roo-to-zoo-migration)에서 확인할 수 있어. 전환하는 동안 사용자들을 최대한 돕고 싶고, 바로 그 지원을 위해 [Reddit](https://www.reddit.com/r/ZooCode)와 [Discord](https://discord.gg/VxfP4Vx3gX)를 운영하고 있어. 문제가 있거나 궁금한 점이 있으면 들어와서 편하게 물어봐. -## v3.68.0의 새로운 기능 +## v3.70.0의 새로운 기능 -- **GLM-5.2를 지원하는 Friendli 프로바이더** — Friendli를 통해 최신 GLM 모델을 사용할 수 있습니다. -- **Ollama용 네이티브 thinking/reasoning 지원** — Ollama 모델을 사용할 때 reasoning 출력을 처음부터 끝까지 유지합니다. -- **Anthropic 사용자 지정 `apiModelId` 수정** — 사용자 지정 Anthropic 모델 ID가 이제 조용히 `claude-sonnet-4-5`로 되돌아가지 않고 선택된 상태로 유지됩니다. -- 수정: Ollama 프로바이더의 도구 결과 처리 및 너무 이른 컨텍스트 압축. -- 수정: 위임된 하위 작업이 중단될 때 부모-자식 작업 연결을 유지합니다. -- Anthropic Vertex Claude 콘텐츠 블록 처리를 개선해 더 안정적인 응답을 제공합니다. -- CI, nightly 배포, 의존성/툴링 업데이트. +- **OpenAI GPT-5.6 패밀리** — `Sol`, `Terra`, `Luna`가 이제 OpenAI Codex와 OpenAI Native 프로바이더 경로 모두에서 사용 가능합니다. +- **Grok 4.5 지원** — xAI의 새로운 플래그십 모델과, Grok 4 Mini에도 도움이 되는 reasoning-effort 형식 수정. +- **Kenari 프로바이더 지원** — Claude, GPT, DeepSeek, GLM, Kimi 등을 지원하며 루피아로 청구되는 일류 OpenAI 호환 AI 게이트웨이. +- 접힌 작업 헤더에 컨텍스트 압축 버튼과 컨텍스트 윈도우 진행률 표시줄을 표시합니다. +- 수정: 콜드 터미널에서의 터미널 출력 손실 및 조기 작업 완료 문제. +- 수정: Zoo Gateway 및 Vercel AI Gateway 모델의 이미지 첨부가 이제 정적 허용 목록 대신 실시간 vision 지원 태그를 따릅니다. +- 의존성 및 툴링 업데이트. ## Zoo Code가 당신을 위해 무엇을 할 수 있을까요? diff --git a/locales/nl/README.md b/locales/nl/README.md index de7cdd352b..7ed26a5495 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -16,13 +16,19 @@ ## Wij zijn Zoo Code -> Je hebt misschien de [recente aankondiging](https://x.com/mattrubens/status/2046636598859559114) van het Roo-team 🦘🦘🦘 gezien. Kort gezegd is het team de actieve ontwikkeling van Roo Code geleidelijk aan het afbouwen terwijl het zich richt op [Roomote](https://roomote.dev/). Dat nieuws was moeilijk voor veel Roo-gebruikers; deze plugin betekent heel veel voor deze community. +> Zoo Code zet de ontwikkeling van dit project voort nadat het Roo-team +> gestopt is met actieve ontwikkeling van Roo Code om zich te richten op +> [Roomote](https://roomote.dev/). Dank aan het Roo-team voor alles wat ze +> hebben gebouwd. > -> We willen het hele Roo-team bedanken voor het werk dat ze in deze plugin hebben gestoken. We gaan hier niet iedereen afzonderlijk bij naam noemen, maar we kunnen het er allemaal over eens zijn dat ze uitzonderlijke ontwikkelaars en, net zo belangrijk, ongelooflijke mensen zijn. Dank aan het Roo-team. -> -> Roo-gebruikers komen in alle soorten en maten. Sommigen gebruiken het professioneel in hun dagelijkse werk, anderen gebruiken het om onvoorstelbaar complexe workflows uit te denken. Sommigen gebruiken het om Roo zelf te verbeteren, terwijl anderen de modellen verbeteren die Roo gebruikt (super meta). Het punt is dat de community divers is, en hoewel een kangoeroe 🦘🦘🦘 een indrukwekkend en nobel dier is, vonden we dat een "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 die diversiteit van plugingebruikers beter weerspiegelde. -> -> Daarom willen we aankondigen dat **Zoo Code** de ontwikkeling van dit belangrijke project zal voortzetten. Het kernteam bestaat uit ontwikkelaars die eerder aan Roo hebben bijgedragen en veel om deze plugin geven. We blijven modellen updaten, bugs oplossen en functies uitbrengen. Maar bovenal willen we luisteren naar jullie, de community die deze plugin zo bijzonder heeft gemaakt. Voel je dus vrij om lid te worden van onze [Discord](https://discord.gg/VxfP4Vx3gX), onze [Reddit](https://www.reddit.com/r/ZooCode), of [een PR of issue te openen](https://github.com/Zoo-Code-Org/Zoo-Code); maar vooral vragen we je om betrokken, verbonden en actief te blijven als community. +> Het kernteam bestaat uit ontwikkelaars die eerder aan Roo hebben +> bijgedragen en veel om deze plugin geven. We blijven modellen updaten, +> bugs oplossen en functies uitbrengen, en we zijn van plan goed te +> luisteren naar de community die deze plugin zo bijzonder heeft gemaakt. +> Doe met ons mee op +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), of +> [open een PR of issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de [Roo→Zoo-migratiegids](https://docs.zoocode.dev/roo-to-zoo-migration). We willen gebruikers zo goed mogelijk helpen tijdens de overgang, en precies daarvoor zijn onze [Reddit](https://www.reddit.com/r/ZooCode) en [Discord](https://discord.gg/VxfP4Vx3gX) er. Als je ergens tegenaan loopt of vragen hebt, kom langs en vraag het. -## Nieuw in v3.68.0 +## Nieuw in v3.70.0 -- **Friendli-provider met GLM-5.2-ondersteuning** — gebruik het nieuwste GLM-model via Friendli. -- **Native thinking/reasoning-ondersteuning voor Ollama** — behoud reasoning-uitvoer van begin tot eind wanneer je Ollama-modellen gebruikt. -- **Fix voor aangepaste Anthropic-`apiModelId`** — aangepaste Anthropic-model-ID's blijven nu geselecteerd in plaats van stilletjes terug te vallen op `claude-sonnet-4-5`. -- Fix: afhandeling van toolresultaten in de Ollama-provider en voortijdige contextcompressie. -- Fix: behoud de ouder-kind-koppeling van taken wanneer een gedelegeerde subtaak wordt onderbroken. -- Verbeterde verwerking van Claude-contentblokken in Anthropic Vertex voor betrouwbaardere antwoorden. -- Updates voor CI, nightly-publicatie en dependencies/tooling. +- **OpenAI GPT-5.6-familie** — `Sol`, `Terra` en `Luna` zijn nu beschikbaar via zowel OpenAI Codex als OpenAI Native. +- **Grok 4.5-ondersteuning** — het nieuwe vlaggenschipmodel van xAI, plus een fix voor het reasoning-effort-formaat waar ook Grok 4 Mini baat bij heeft. +- **Kenari-providerondersteuning** — een eersteklas, OpenAI-compatibele AI-gateway die in Roepia factureert en Claude, GPT, DeepSeek, GLM, Kimi en meer dekt. +- Toon de contextcompressie-knop en de voortgangsbalk van het contextvenster in de ingeklapte taakheader. +- Fix: verlies van terminaluitvoer en voortijdige taakvoltooiing bij koude terminals. +- Fix: afbeeldingen bijvoegen voor Zoo Gateway- en Vercel AI Gateway-modellen volgt nu live vision-capaciteitstags in plaats van een statische allowlist. +- Updates van dependencies en tooling. ## Wat kan Zoo Code voor JOU doen? diff --git a/locales/pl/README.md b/locales/pl/README.md index deba8b5bca..cc5473b330 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -16,13 +16,17 @@ ## Jesteśmy Zoo Code -> Być może widziałeś [niedawne ogłoszenie](https://x.com/mattrubens/status/2046636598859559114) zespołu Roo 🦘🦘🦘. W skrócie: zespół stopniowo wygasza aktywny rozwój Roo Code, skupiając się na [Roomote](https://roomote.dev/). Ta wiadomość była trudna dla wielu użytkowników Roo; ta wtyczka naprawdę wiele znaczy dla tej społeczności. +> Zoo Code kontynuuje rozwój tego projektu po tym, jak zespół Roo zakończył +> aktywny rozwój Roo Code, aby skupić się na [Roomote](https://roomote.dev/). +> Dziękujemy zespołowi Roo za wszystko, co stworzyli. > -> Chcemy podziękować całemu zespołowi Roo za pracę włożoną w tę wtyczkę. Nie będziemy tutaj wymieniać każdej osoby z osobna, ale wszyscy możemy się zgodzić, że to wyjątkowi deweloperzy i, co równie ważne, niesamowici ludzie. Dziękujemy zespołowi Roo. -> -> Użytkownicy Roo są bardzo różni. Jedni używają go zawodowo na co dzień, inni wykorzystują go do tworzenia niewiarygodnie złożonych workflow. Niektórzy używają go do ulepszania samego Roo, a inni do ulepszania modeli, z których Roo korzysta (super meta). Chodzi o to, że społeczność jest różnorodna, i choć kangur 🦘🦘🦘 jest dostojnym i szlachetnym zwierzęciem, uznaliśmy, że „Zoo” 🐘🦡🦒🦓🦛🦧🦭🦦 lepiej oddaje tę różnorodność użytkowników wtyczki. -> -> Dlatego chcemy ogłosić, że **Zoo Code** będzie kontynuować rozwój tego ważnego projektu. Główny zespół tworzą deweloperzy, którzy wcześniej współtworzyli Roo i naprawdę zależy im na tej wtyczce. Będziemy dalej aktualizować modele, naprawiać błędy i wydawać nowe funkcje. Ale przede wszystkim chcemy słuchać was, społeczności, która uczyniła tę wtyczkę tak wyjątkową. Dołącz więc do naszego [Discorda](https://discord.gg/VxfP4Vx3gX), naszego [Reddita](https://www.reddit.com/r/ZooCode) albo [otwórz PR lub issue](https://github.com/Zoo-Code-Org/Zoo-Code); a przede wszystkim prosimy cię, żebyś pozostał zaangażowany, w kontakcie i aktywny jako część społeczności. +> Główny zespół to grupa deweloperów, którzy wcześniej współtworzyli Roo i +> naprawdę zależy im na tej wtyczce. Będziemy dalej aktualizować modele, +> naprawiać błędy i wydawać nowe funkcje, i zamierzamy uważnie słuchać +> społeczności, która uczyniła tę wtyczkę tak wyjątkową. Dołącz do nas na +> [Discordzie](https://discord.gg/VxfP4Vx3gX), +> [Reddicie](https://www.reddit.com/r/ZooCode), albo +> [otwórz PR lub issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +34,15 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku migracji Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chcemy jak najlepiej pomagać użytkownikom w czasie przejścia i właśnie do tego służą nasze [Reddit](https://www.reddit.com/r/ZooCode) oraz [Discord](https://discord.gg/VxfP4Vx3gX). Jeśli masz problem albo pytanie, wpadaj i pytaj. -## Nowości w v3.68.0 +## Nowości w v3.70.0 -- **Provider Friendli z obsługą GLM-5.2** — używaj najnowszego modelu GLM przez Friendli. -- **Natywna obsługa thinking/reasoning dla Ollamy** — zachowuj wynik rozumowania od początku do końca podczas korzystania z modeli Ollama. -- **Poprawka niestandardowego `apiModelId` Anthropic** — niestandardowe identyfikatory modeli Anthropic pozostają teraz wybrane zamiast po cichu wracać do `claude-sonnet-4-5`. -- Poprawka: obsługa wyników narzędzi w providerze Ollama i przedwczesne kondensowanie kontekstu. -- Poprawka: zachowuj powiązanie zadania nadrzędnego i podrzędnego, gdy delegowane podzadanie zostanie przerwane. -- Ulepszona obsługa bloków treści Claude w Anthropic Vertex dla bardziej niezawodnych odpowiedzi. -- Aktualizacje CI, publikacji nightly oraz zależności/narzędzi. +- **Rodzina OpenAI GPT-5.6** — `Sol`, `Terra` i `Luna` są teraz dostępne zarówno w ścieżce providera OpenAI Codex, jak i OpenAI Native. +- **Obsługa Grok 4.5** — nowy flagowy model xAI, plus poprawka formatu reasoning-effort, z której korzysta też Grok 4 Mini. +- **Obsługa providera Kenari** — pierwszorzędna, kompatybilna z OpenAI bramka AI rozliczana w rupiach, obejmująca Claude, GPT, DeepSeek, GLM, Kimi i inne. +- Wyświetlanie przycisku kondensacji kontekstu i paska postępu okna kontekstu w zwiniętym nagłówku zadania. +- Poprawka: utrata danych wyjściowych terminala i przedwczesne kończenie zadań na zimnych terminalach. +- Poprawka: dołączanie obrazów dla modeli Zoo Gateway i Vercel AI Gateway korzysta teraz z bieżących tagów możliwości vision zamiast statycznej listy dozwolonych. +- Aktualizacje zależności i narzędzi. ## Co Zoo Code może zrobić dla CIEBIE? diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 5f6c10e81e..90f94199cb 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -16,13 +16,19 @@ ## Somos Zoo Code -> Você pode ter visto o [anúncio recente](https://x.com/mattrubens/status/2046636598859559114) da equipe do Roo 🦘🦘🦘. O resumo é que a equipe está reduzindo gradualmente o desenvolvimento ativo do Roo Code enquanto se concentra no [Roomote](https://roomote.dev/). Essa notícia foi difícil para muitos usuários do Roo; esse plugin significa muito para esta comunidade. +> O Zoo Code dá continuidade ao desenvolvimento deste projeto depois que a +> equipe do Roo encerrou o desenvolvimento ativo do Roo Code para se +> concentrar no [Roomote](https://roomote.dev/). Obrigado à equipe do Roo +> por tudo o que construíram. > -> Queremos agradecer a toda a equipe do Roo pelo trabalho que colocou neste plugin. Não vamos citar cada pessoa pelo nome aqui, mas todos podemos concordar que eles são desenvolvedores excepcionais e, tão importante quanto isso, pessoas incríveis. Obrigado à equipe do Roo. -> -> Os usuários do Roo vêm de todos os tipos de contextos. Alguns usam profissionalmente no trabalho do dia a dia, outros usam para montar workflows inimaginavelmente complexos. Alguns usam para melhorar o próprio Roo, enquanto outros usam para melhorar os próprios modelos que o Roo utiliza (super meta). O ponto é que a comunidade é diversa e, embora um canguru 🦘🦘🦘 seja um animal distinto e nobre, sentimos que um "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 refletia melhor essa diversidade dos usuários do plugin. -> -> Por isso, queremos anunciar que **Zoo Code** continuará o desenvolvimento deste projeto importante. A equipe principal é formada por desenvolvedores que já contribuíram com o Roo antes e se importam profundamente com esse plugin. Vamos continuar fazendo atualizações de modelos, corrigindo bugs e lançando recursos. Mas, acima de tudo, queremos ouvir vocês, a comunidade que tornou esse plugin tão especial. Então fique à vontade para entrar no nosso [Discord](https://discord.gg/VxfP4Vx3gX), no nosso [Reddit](https://www.reddit.com/r/ZooCode), ou [abrir um PR ou uma issue](https://github.com/Zoo-Code-Org/Zoo-Code); mas, mais do que tudo, pedimos que você continue envolvido, conectado e ativo como comunidade. +> A equipe principal é formada por desenvolvedores que já contribuíram com +> o Roo antes e se importam profundamente com esse plugin. Vamos continuar +> fazendo atualizações de modelos, corrigindo bugs e lançando recursos, e +> pretendemos ouvir com atenção a comunidade que tornou esse plugin tão +> especial. Junte-se a nós no +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), ou +> [abra um PR ou issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia de migração Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ajudar os usuários durante essa transição da melhor forma possível, e é exatamente para isso que temos nosso [Reddit](https://www.reddit.com/r/ZooCode) e nosso [Discord](https://discord.gg/VxfP4Vx3gX). Se você tiver algum problema ou dúvida, apareça por lá e pergunte. -## Novidades na v3.68.0 +## Novidades na v3.70.0 -- **Provedor Friendli com suporte a GLM-5.2** — use o modelo GLM mais recente pelo Friendli. -- **Suporte nativo de thinking/reasoning para Ollama** — preserve a saída de raciocínio de ponta a ponta ao usar modelos do Ollama. -- **Correção de `apiModelId` personalizado da Anthropic** — os IDs de modelo personalizados da Anthropic agora permanecem selecionados em vez de voltar silenciosamente para `claude-sonnet-4-5`. -- Correção: tratamento de resultados de ferramentas do provedor Ollama e condensação prematura de contexto. -- Correção: preserve o vínculo pai-filho da tarefa quando uma subtarefa delegada é interrompida. -- Melhora o tratamento de blocos de conteúdo Claude no Anthropic Vertex para respostas mais confiáveis. -- Atualizações de CI, publicação nightly e dependências/tooling. +- **Família OpenAI GPT-5.6** — `Sol`, `Terra` e `Luna` já estão disponíveis tanto pelo OpenAI Codex quanto pelo OpenAI Native. +- **Suporte a Grok 4.5** — o novo modelo principal da xAI, além de uma correção no formato de reasoning-effort que também beneficia o Grok 4 Mini. +- **Suporte ao provedor Kenari** — um gateway de IA de primeira linha, compatível com OpenAI, cobrado em rupias, cobrindo Claude, GPT, DeepSeek, GLM, Kimi e mais. +- Exibe o botão de condensação de contexto e a barra de progresso da janela de contexto no cabeçalho de tarefa recolhido. +- Correção: perda de saída do terminal e conclusão prematura de tarefas em terminais frios. +- Correção: o anexo de imagens para os modelos Zoo Gateway e Vercel AI Gateway agora segue as tags de capacidade de visão em tempo real, em vez de uma allowlist estática. +- Atualizações de dependências e ferramentas. ## O que o Zoo Code pode fazer por VOCÊ? diff --git a/locales/ru/README.md b/locales/ru/README.md index 850b25b3e0..35e2541ce8 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -16,13 +16,19 @@ ## Мы — Zoo Code -> Возможно, ты видел [недавнее объявление](https://x.com/mattrubens/status/2046636598859559114) команды Roo 🦘🦘🦘. Коротко: команда постепенно сворачивает активную разработку Roo Code, сосредоточившись на [Roomote](https://roomote.dev/). Эта новость тяжело далась многим пользователям Roo; этот плагин очень много значит для этого сообщества. +> Zoo Code продолжает разработку этого проекта после того, как команда Roo +> прекратила активную разработку Roo Code, чтобы сосредоточиться на +> [Roomote](https://roomote.dev/). Спасибо команде Roo за всё, что они +> создали. > -> Мы хотим поблагодарить всю команду Roo за работу, которую они вложили в этот плагин. Мы не будем перечислять здесь каждого по имени, но все можем согласиться, что это выдающиеся разработчики и, что не менее важно, невероятные люди. Спасибо команде Roo. -> -> Пользователи Roo очень разные. Кто-то использует его профессионально каждый день, кто-то — чтобы придумывать невероятно сложные workflow. Кто-то помогает улучшать сам Roo, а кто-то — модели, которые использует Roo (супер мета). Суть в том, что сообщество разнообразно, и хотя кенгуру 🦘🦘🦘 — благородное и достойное животное, нам показалось, что «Zoo» 🐘🦡🦒🦓🦛🦧🦭🦦 лучше отражает это разнообразие пользователей плагина. -> -> Поэтому мы хотим объявить, что **Zoo Code** продолжит развитие этого важного проекта. Основная команда состоит из разработчиков, которые раньше уже вносили вклад в Roo и которым действительно небезразличен этот плагин. Мы продолжим обновлять модели, исправлять ошибки и выпускать новые функции. Но больше всего мы хотим слушать вас — сообщество, которое сделало этот плагин таким особенным. Так что присоединяйся к нашему [Discord](https://discord.gg/VxfP4Vx3gX), нашему [Reddit](https://www.reddit.com/r/ZooCode), или [открывай PR или issue](https://github.com/Zoo-Code-Org/Zoo-Code); но прежде всего мы просим тебя оставаться вовлеченным, на связи и активным как часть сообщества. +> Основная команда состоит из разработчиков, которые раньше уже вносили +> вклад в Roo и которым действительно небезразличен этот плагин. Мы +> продолжим обновлять модели, исправлять ошибки и выпускать новые функции, +> и планируем внимательно слушать сообщество, которое сделало этот плагин +> таким особенным. Присоединяйся к нам в +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), или +> [открой PR или issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +36,15 @@ Короткое руководство по переходу с Roo Code на Zoo Code можно найти в [гайде по миграции Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Мы хотим как можно лучше помочь пользователям во время перехода, и именно для этого у нас есть [Reddit](https://www.reddit.com/r/ZooCode) и [Discord](https://discord.gg/VxfP4Vx3gX). Если у тебя возникнут проблемы или вопросы, заходи и спрашивай. -## Что нового в v3.68.0 +## Что нового в v3.70.0 -- **Провайдер Friendli с поддержкой GLM-5.2** — используй новейшую модель GLM через Friendli. -- **Нативная поддержка thinking/reasoning для Ollama** — сохраняй вывод рассуждений от начала до конца при использовании моделей Ollama. -- **Исправление пользовательского `apiModelId` Anthropic** — пользовательские идентификаторы моделей Anthropic теперь остаются выбранными вместо тихого возврата к `claude-sonnet-4-5`. -- Исправление: обработка результатов инструментов провайдера Ollama и преждевременное сжатие контекста. -- Исправление: сохраняй связь родительской и дочерней задачи, когда делегированная подзадача прерывается. -- Улучшена обработка блоков контента Claude в Anthropic Vertex для более надёжных ответов. -- Обновления CI, nightly-публикации и зависимостей/инструментов. +- **Семейство OpenAI GPT-5.6** — `Sol`, `Terra` и `Luna` теперь доступны как через OpenAI Codex, так и через OpenAI Native. +- **Поддержка Grok 4.5** — новая флагманская модель xAI, а также исправление формата reasoning-effort, от которого также выигрывает Grok 4 Mini. +- **Поддержка провайдера Kenari** — первоклассный, совместимый с OpenAI ИИ-шлюз с оплатой в рупиях, охватывающий Claude, GPT, DeepSeek, GLM, Kimi и другие модели. +- Отображение кнопки сжатия контекста и индикатора заполнения контекстного окна в свёрнутом заголовке задачи. +- Исправление: потеря вывода терминала и преждевременное завершение задачи на «холодных» терминалах. +- Исправление: прикрепление изображений для моделей Zoo Gateway и Vercel AI Gateway теперь следует актуальным тегам поддержки vision вместо статического allowlist. +- Обновления зависимостей и инструментов. ## Что Zoo Code может сделать для ВАС? diff --git a/locales/tr/README.md b/locales/tr/README.md index d95e0e0c5d..3dd0413340 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -16,29 +16,35 @@ ## Biz Zoo Code'uz -> Roo ekibinin 🦘🦘🦘 [son duyurusunu](https://x.com/mattrubens/status/2046636598859559114) görmüş olabilirsin. Kısacası ekip, [Roomote](https://roomote.dev/) üzerine odaklanırken Roo Code'un aktif gelişimini kademeli olarak azaltıyor. Bu haber birçok Roo kullanıcısı için zordu; bu eklenti bu topluluk için çok şey ifade ediyor. +> Roo ekibi, [Roomote](https://roomote.dev/) üzerine odaklanmak için Roo +> Code'un aktif geliştirmesini durdurduktan sonra Zoo Code bu projenin +> geliştirilmesini sürdürüyor. İnşa ettikleri her şey için Roo ekibine +> teşekkürler. > -> Bu eklentiye verdikleri emek için tüm Roo ekibine teşekkür etmek istiyoruz. Burada herkesin adını tek tek anmayacağız, ama hepimiz onların olağanüstü geliştiriciler ve aynı derecede inanılmaz insanlar olduğu konusunda hemfikiriz. Roo ekibine teşekkürler. -> -> Roo kullanıcıları her türden geliyor. Bazıları bunu günlük işlerinde profesyonel olarak kullanıyor, bazılarıysa akıl almaz derecede karmaşık iş akışları tasarlamak için kullanıyor. Bazıları Roo'nun kendisini geliştirirken, bazıları da Roo'nun kullandığı modelleri geliştiriyor (tam anlamıyla meta). Demek istediğimiz şu: topluluk çok çeşitli ve bir kanguru 🦘🦘🦘 seçkin ve asil bir hayvan olsa da farklı türlerden oluşan bir "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 bu eklentinin kullanıcı çeşitliliğini daha iyi yansıtıyor. -> -> Bu yüzden **Zoo Code**'un bu önemli projenin geliştirilmesini sürdüreceğini duyurmak istiyoruz. Çekirdek ekip, daha önce Roo'ya katkıda bulunmuş ve bu eklentiye gerçekten önem veren geliştiricilerden oluşuyor. Model güncellemeleri yapmaya, hataları düzeltmeye ve özellikler yayınlamaya devam edeceğiz. Ama her şeyden önce, bu eklentiyi özel kılan topluluğun sesi olan seni dinlemek istiyoruz. Bu yüzden [Discord](https://discord.gg/VxfP4Vx3gX) sunucumuza, [Reddit](https://www.reddit.com/r/ZooCode) topluluğumuza katılabilir ya da [PR veya issue açabilirsin](https://github.com/Zoo-Code-Org/Zoo-Code); ama en önemlisi, topluluk olarak ilgili, bağlantıda ve aktif kalmanı istiyoruz. +> Çekirdek ekip, daha önce Roo'ya katkıda bulunmuş ve bu eklentiye gerçekten +> önem veren geliştiricilerden oluşuyor. Model güncellemeleri yapmaya, +> hataları düzeltmeye ve özellikler yayınlamaya devam edeceğiz ve bu +> eklentiyi bu kadar özel kılan topluluğu yakından dinlemeyi planlıyoruz. +> Bize katıl: +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), ya da +> [PR veya issue aç](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ ## Roo Code'dan Zoo Code'a geçiş -Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da sorunun olursa gel ve sor. +Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da soruların olursa gel ve sor. -## v3.68.0'daki Yenilikler +## v3.70.0'daki Yenilikler -- **GLM-5.2 destekli Friendli sağlayıcısı** — en yeni GLM modelini Friendli üzerinden kullan. -- **Ollama için yerel thinking/reasoning desteği** — Ollama modellerini kullanırken reasoning çıktısını uçtan uca koru. -- **Anthropic özel `apiModelId` düzeltmesi** — özel Anthropic model kimlikleri artık sessizce `claude-sonnet-4-5` modeline geri dönmek yerine seçili kalıyor. -- Düzeltme: Ollama sağlayıcısında araç sonucu işleme ve erken bağlam yoğunlaştırma. -- Düzeltme: yetkilendirilen bir alt görev kesintiye uğradığında görev üst-alt bağlantısını koru. -- Daha güvenilir yanıtlar için Anthropic Vertex Claude içerik bloğu işleme iyileştirildi. -- CI, nightly yayınlama ve bağımlılık/araç güncellemeleri. +- **OpenAI GPT-5.6 ailesi** — `Sol`, `Terra` ve `Luna` artık hem OpenAI Codex hem de OpenAI Native sağlayıcı yollarında kullanılabiliyor. +- **Grok 4.5 desteği** — xAI'nin yeni amiral gemisi modeli, ayrıca Grok 4 Mini'ye de fayda sağlayan bir reasoning-effort format düzeltmesi. +- **Kenari sağlayıcı desteği** — Claude, GPT, DeepSeek, GLM, Kimi ve daha fazlasını kapsayan, Rupi ile faturalandırılan, OpenAI uyumlu, birinci sınıf bir AI ağ geçidi. +- Daraltılmış görev başlığında bağlam yoğunlaştırma düğmesi ve bağlam penceresi ilerleme çubuğunu göster. +- Düzeltme: soğuk terminallerde terminal çıktısı kaybı ve erken görev tamamlanması. +- Düzeltme: Zoo Gateway ve Vercel AI Gateway modelleri için görsel ekleme artık statik bir izin listesi yerine canlı görme yeteneği etiketlerini takip ediyor. +- Bağımlılık ve araç güncellemeleri. ## Zoo Code SİZİN İçin Ne Yapabilir? diff --git a/locales/vi/README.md b/locales/vi/README.md index 27217bc3bc..86740a8795 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -16,13 +16,18 @@ ## Chúng tôi là Zoo Code -> Có thể bạn đã thấy [thông báo gần đây](https://x.com/mattrubens/status/2046636598859559114) từ đội ngũ Roo 🦘🦘🦘. Tóm lại, đội ngũ đang dần giảm việc phát triển Roo Code một cách tích cực để tập trung vào [Roomote](https://roomote.dev/). Tin này thật khó chấp nhận với nhiều người dùng Roo; plugin này có ý nghĩa rất lớn với cộng đồng này. +> Zoo Code tiếp tục phát triển dự án này sau khi đội ngũ Roo dừng việc phát +> triển tích cực Roo Code để tập trung vào [Roomote](https://roomote.dev/). +> Cảm ơn đội ngũ Roo vì tất cả những gì họ đã xây dựng. > -> Chúng tôi muốn cảm ơn toàn bộ đội ngũ Roo vì công sức họ đã dành cho plugin này. Chúng tôi sẽ không nêu tên từng người ở đây, nhưng tất cả chúng ta đều có thể đồng ý rằng họ là những nhà phát triển xuất sắc và, quan trọng không kém, là những con người tuyệt vời. Cảm ơn đội ngũ Roo. -> -> Người dùng Roo đến từ đủ mọi kiểu khác nhau. Có người dùng nó chuyên nghiệp trong công việc hằng ngày, có người dùng nó để nghĩ ra những workflow phức tạp đến khó tin. Có người dùng nó để cải thiện chính Roo, trong khi người khác dùng nó để cải thiện những model mà Roo đang sử dụng (siêu meta). Điều chúng tôi muốn nói là cộng đồng rất đa dạng, và dù một chú kangaroo 🦘🦘🦘 là một loài vật cao quý và nổi bật, chúng tôi cảm thấy một "Zoo" 🐘🦡🦒🦓🦛🦧🦭🦦 với nhiều loài khác nhau phản ánh sự đa dạng đó tốt hơn. -> -> Vì vậy, chúng tôi muốn thông báo rằng **Zoo Code** sẽ tiếp tục phát triển dự án quan trọng này. Đội ngũ nòng cốt gồm những nhà phát triển từng đóng góp cho Roo trước đây và thực sự quan tâm đến plugin này. Chúng tôi sẽ tiếp tục cập nhật model, sửa lỗi và phát hành tính năng. Nhưng trên hết, chúng tôi muốn lắng nghe bạn, cộng đồng đã làm cho plugin này trở nên đặc biệt. Vì vậy, hãy tham gia [Discord](https://discord.gg/VxfP4Vx3gX), [Reddit](https://www.reddit.com/r/ZooCode) của chúng tôi, hoặc [tạo PR hay issue](https://github.com/Zoo-Code-Org/Zoo-Code); nhưng hơn hết, chúng tôi mong bạn tiếp tục gắn bó, kết nối và năng động cùng cộng đồng. +> Đội ngũ nòng cốt gồm những nhà phát triển từng đóng góp cho Roo trước đây +> và thực sự quan tâm đến plugin này. Chúng tôi sẽ tiếp tục cập nhật model, +> sửa lỗi và phát hành tính năng, và chúng tôi dự định lắng nghe sát sao +> cộng đồng đã làm cho plugin này trở nên đặc biệt. Hãy tham gia cùng chúng +> tôi trên +> [Discord](https://discord.gg/VxfP4Vx3gX), +> [Reddit](https://www.reddit.com/r/ZooCode), hoặc +> [mở PR hay issue](https://github.com/Zoo-Code-Org/Zoo-Code). > > _-Zoo Code Team_ @@ -30,15 +35,15 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo Code trong [hướng dẫn chuyển đổi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chúng tôi muốn hỗ trợ người dùng nhiều nhất có thể trong quá trình chuyển đổi, và đó chính là lý do chúng tôi có [Reddit](https://www.reddit.com/r/ZooCode) và [Discord](https://discord.gg/VxfP4Vx3gX). Nếu bạn gặp vấn đề hoặc có câu hỏi, cứ vào hỏi nhé. -## Điểm mới trong v3.68.0 +## Điểm mới trong v3.70.0 -- **Nhà cung cấp Friendli với hỗ trợ GLM-5.2** — dùng mẫu GLM mới nhất qua Friendli. -- **Hỗ trợ thinking/reasoning gốc cho Ollama** — giữ đầu ra suy luận xuyên suốt khi bạn dùng các mẫu Ollama. -- **Sửa lỗi `apiModelId` tùy chỉnh của Anthropic** — ID mẫu Anthropic tùy chỉnh giờ sẽ tiếp tục được chọn thay vì âm thầm rơi về `claude-sonnet-4-5`. -- Sửa lỗi: xử lý kết quả công cụ của nhà cung cấp Ollama và cô đọng ngữ cảnh quá sớm. -- Sửa lỗi: giữ liên kết cha-con của tác vụ khi một tác vụ con được ủy quyền bị gián đoạn. -- Cải thiện xử lý khối nội dung Claude trên Anthropic Vertex để có phản hồi đáng tin cậy hơn. -- Cập nhật CI, phát hành nightly và phụ thuộc/công cụ. +- **Dòng OpenAI GPT-5.6** — `Sol`, `Terra`, và `Luna` hiện đã có sẵn trên cả hai đường nhà cung cấp OpenAI Codex và OpenAI Native. +- **Hỗ trợ Grok 4.5** — model chủ lực mới của xAI, cùng với bản sửa lỗi định dạng reasoning-effort cũng mang lại lợi ích cho Grok 4 Mini. +- **Hỗ trợ nhà cung cấp Kenari** — một cổng AI hạng nhất, tương thích OpenAI, tính phí bằng Rupiah, bao phủ Claude, GPT, DeepSeek, GLM, Kimi và nhiều hơn nữa. +- Hiển thị nút cô đọng ngữ cảnh và thanh tiến trình cửa sổ ngữ cảnh trong tiêu đề tác vụ đã thu gọn. +- Sửa lỗi: mất đầu ra terminal và hoàn thành tác vụ quá sớm trên các terminal nguội. +- Sửa lỗi: đính kèm hình ảnh cho các model Zoo Gateway và Vercel AI Gateway giờ đây tuân theo các thẻ khả năng vision trực tiếp thay vì danh sách cho phép tĩnh. +- Cập nhật phụ thuộc và công cụ. ## Zoo Code có thể làm gì cho BẠN? diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 7b6b88a5de..9a3e44c155 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -16,13 +16,15 @@ ## 我们是 Zoo Code -> 你可能已经看到 Roo 团队 🦘🦘🦘 的[最近公告](https://x.com/mattrubens/status/2046636598859559114)。简单来说,团队在把重心放到 [Roomote](https://roomote.dev/) 的同时,正在逐步减少 Roo Code 的活跃开发。这个消息让很多 Roo 用户都很难受;这个插件对这个社区意义重大。 +> 在 Roo 团队停止 Roo Code 的积极开发、转而专注于 [Roomote](https://roomote.dev/) +> 之后,Zoo Code 将继续开发这个项目。感谢 Roo 团队所构建的一切。 > -> 我们想感谢整个 Roo 团队为这个插件投入的努力。这里不会逐一点名,但大家都能认同,他们既是卓越的开发者,也是同样令人敬佩的优秀的人。感谢 Roo 团队。 -> -> Roo 用户来自各种不同背景。有人在日常工作中专业使用它,也有人用它来折腾难以想象的复杂 workflow。有人用它来改进 Roo 本身,也有人用它来改进 Roo 所使用的模型(非常 meta)。我们想表达的是,这个社区非常多元。虽然袋鼠 🦘🦘🦘 是一种高贵又有辨识度的动物,但我们觉得由不同物种组成的“Zoo” 🐘🦡🦒🦓🦛🦧🦭🦦 更能体现插件用户的多样性。 -> -> 所以我们想宣布,**Zoo Code** 将继续开发这个重要项目。核心团队由此前曾为 Roo 做出贡献、并且非常在乎这个插件的开发者组成。我们会继续更新模型、修复 bug,并发布新功能。但最重要的是,我们会认真倾听你们——让这个插件如此特别的社区。欢迎加入我们的 [Discord](https://discord.gg/VxfP4Vx3gX)、[Reddit](https://www.reddit.com/r/ZooCode),或者[创建一个 PR 或 issue](https://github.com/Zoo-Code-Org/Zoo-Code);但更重要的是,我们希望你作为社区的一员继续保持参与、保持连接、保持活跃。 +> 核心团队由此前曾为 Roo 做出贡献、并且非常在乎这个插件的开发者组成。我们 +> 会继续更新模型、修复 bug,并发布新功能,也计划认真倾听让这个插件如此特 +> 别的社区。欢迎加入我们,一起在 +> [Discord](https://discord.gg/VxfP4Vx3gX)、 +> [Reddit](https://www.reddit.com/r/ZooCode),或者 +> [创建 PR 或 issue](https://github.com/Zoo-Code-Org/Zoo-Code)。 > > _-Zoo Code Team_ @@ -30,15 +32,15 @@ 你可以在 [Roo→Zoo 迁移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到从 Roo Code 迁移到 Zoo Code 的快速说明。我们希望在大家迁移过程中尽可能提供帮助,这也是我们设立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社区的原因。如果你遇到问题或有任何疑问,欢迎加入后直接提问。 -## v3.68.0 新增内容 +## v3.70.0 新增内容 -- **支持 GLM-5.2 的 Friendli 提供商** — 通过 Friendli 使用最新的 GLM 模型。 -- **Ollama 原生 thinking/reasoning 支持** — 使用 Ollama 模型时可端到端保留 reasoning 输出。 -- **Anthropic 自定义 `apiModelId` 修复** — 自定义 Anthropic 模型 ID 现在会保持选中,不再静默回退到 `claude-sonnet-4-5`。 -- 修复:Ollama 提供商的工具结果处理和过早的上下文压缩。 -- 修复:委派子任务被中断时保留父子任务链接。 -- 改进 Anthropic Vertex 中 Claude 内容块的处理,以获得更可靠的响应。 -- CI、nightly 发布以及依赖/工具更新。 +- **OpenAI GPT-5.6 系列** — `Sol`、`Terra` 和 `Luna` 现已在 OpenAI Codex 和 OpenAI Native 两条提供商路径上可用。 +- **Grok 4.5 支持** — xAI 的全新旗舰模型,以及一项同样惠及 Grok 4 Mini 的 reasoning-effort 格式修复。 +- **Kenari 提供商支持** — 一个一流的、兼容 OpenAI 的 AI 网关,以卢比计费,覆盖 Claude、GPT、DeepSeek、GLM、Kimi 等更多模型。 +- 在折叠的任务标题中显示上下文压缩按钮和上下文窗口进度条。 +- 修复:在冷终端上出现的终端输出丢失和任务过早完成的问题。 +- 修复:Zoo Gateway 和 Vercel AI Gateway 模型的图片附加功能现在会跟随实时的 vision 能力标签,而不是静态的白名单。 +- 依赖和工具更新。 ## Zoo Code 能为您做什么? diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 988c42cbbe..4bced1aa72 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -16,13 +16,15 @@ ## 我們是 Zoo Code -> 你可能已經看到 Roo 團隊 🦘🦘🦘 的[最近公告](https://x.com/mattrubens/status/2046636598859559114)。簡單來說,團隊在把重心放到 [Roomote](https://roomote.dev/) 的同時,正在逐步減少 Roo Code 的活躍開發。這個消息讓很多 Roo 使用者都很難受;這個外掛對這個社群意義重大。 +> 在 Roo 團隊停止 Roo Code 的積極開發、轉而專注於 [Roomote](https://roomote.dev/) +> 之後,Zoo Code 將繼續開發這個專案。感謝 Roo 團隊所建立的一切。 > -> 我們想感謝整個 Roo 團隊為這個外掛投入的努力。這裡不會逐一點名,但大家都能認同,他們既是卓越的開發者,也是同樣令人敬佩的優秀的人。感謝 Roo 團隊。 -> -> Roo 使用者來自各種不同背景。有人在日常工作中專業使用它,也有人用它來構思難以想像的複雜 workflow。有人用它來改進 Roo 本身,也有人用它來改進 Roo 所使用的模型(超級 meta)。我們想表達的是,這個社群非常多元。雖然袋鼠 🦘🦘🦘 是高貴又有代表性的動物,但我們覺得由不同物種組成的「Zoo」🐘🦡🦒🦓🦛🦧🦭🦦 更能反映這個外掛使用者的多樣性。 -> -> 因此我們想宣布,**Zoo Code** 將繼續開發這個重要專案。核心團隊由先前曾為 Roo 做出貢獻、並且非常在乎這個外掛的開發者所組成。我們會持續更新模型、修正 bug,並推出新功能。但最重要的是,我們想傾聽你們——讓這個外掛如此特別的社群。歡迎加入我們的 [Discord](https://discord.gg/VxfP4Vx3gX)、[Reddit](https://www.reddit.com/r/ZooCode),或是[建立 PR 或 issue](https://github.com/Zoo-Code-Org/Zoo-Code);但更重要的是,我們希望你能繼續作為社群的一分子,保持參與、保持連結、保持活躍。 +> 核心團隊由先前曾為 Roo 做出貢獻、並且非常在乎這個外掛的開發者所組成。 +> 我們會持續更新模型、修正 bug,並推出新功能,也計劃仔細傾聽讓這個外掛 +> 如此特別的社群。歡迎加入我們,一起在 +> [Discord](https://discord.gg/VxfP4Vx3gX)、 +> [Reddit](https://www.reddit.com/r/ZooCode),或是 +> [建立 PR 或 issue](https://github.com/Zoo-Code-Org/Zoo-Code)。 > > _-Zoo Code Team_ @@ -30,15 +32,15 @@ 你可以在 [Roo→Zoo 遷移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到從 Roo Code 遷移到 Zoo Code 的快速說明。我們希望在大家轉移過程中盡可能提供協助,這也是我們設立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社群的原因。如果你遇到問題或有任何疑問,歡迎加入後直接提問。 -## v3.68.0 新功能 +## v3.70.0 新功能 -- **支援 GLM-5.2 的 Friendli 供應商** — 透過 Friendli 使用最新的 GLM 模型。 -- **Ollama 原生 thinking/reasoning 支援** — 使用 Ollama 模型時可端對端保留 reasoning 輸出。 -- **Anthropic 自訂 `apiModelId` 修正** — 自訂 Anthropic 模型 ID 現在會保持選取,不再靜默回退到 `claude-sonnet-4-5`。 -- 修正:Ollama 供應商的工具結果處理與過早的內容壓縮。 -- 修正:委派子任務被中斷時保留父子任務連結。 -- 改進 Anthropic Vertex 中 Claude 內容區塊的處理,以獲得更可靠的回應。 -- CI、nightly 發布以及相依套件/工具更新。 +- **OpenAI GPT-5.6 系列** — `Sol`、`Terra` 和 `Luna` 現已在 OpenAI Codex 與 OpenAI Native 兩條供應商路徑上提供。 +- **Grok 4.5 支援** — xAI 全新的旗艦模型,以及一項同樣使 Grok 4 Mini 受惠的 reasoning-effort 格式修正。 +- **Kenari 供應商支援** — 一個一流的、相容 OpenAI 的 AI 閘道,以印尼盾計費,涵蓋 Claude、GPT、DeepSeek、GLM、Kimi 等更多模型。 +- 在摺疊的任務標頭中顯示內容壓縮按鈕與內容視窗進度條。 +- 修正:冷終端機上發生的終端機輸出遺失與任務過早完成問題。 +- 修正:Zoo Gateway 與 Vercel AI Gateway 模型的圖片附加功能現在會依循即時的 vision 能力標籤,而非靜態的白名單。 +- 相依套件與工具更新。 ## Zoo Code 能為您做什麼? diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6e5545a1f2..47e31d7e2d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -213,7 +213,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jul-2026-v3.68.0-friendli-ollama-anthropic-apimodelid" // v3.68.0 Friendli GLM-5.2 support, native Ollama thinking/reasoning, Anthropic custom apiModelId fix + public readonly latestAnnouncementId = "jul-2026-v3.70.0-gpt5.6-grok4.5-kenari" // v3.70.0 GPT-5.6 family, Grok 4.5 support, Kenari provider public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager diff --git a/src/package.json b/src/package.json index 020705effc..7eb12d5fde 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "ZooCodeOrganization", - "version": "3.68.0", + "version": "3.70.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 50cf831543..6e78f21ef2 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -341,9 +341,9 @@ "support": "Dona suport a Zoo Code fent-nos una estrella a GitHub.", "release": { "heading": "Novetats:", - "highlight1": "Proveïdor Friendli amb suport per a GLM-5.2 — utilitza el model GLM més recent a través de Friendli.", - "highlight2": "Suport natiu de thinking/reasoning per a Ollama — conserva la sortida de raonament d'extrem a extrem quan fas servir models d'Ollama.", - "highlight3": "Correcció del `apiModelId` personalitzat d'Anthropic — els identificadors de model personalitzats d'Anthropic ara es mantenen seleccionats en lloc de tornar en silenci a `claude-sonnet-4-5`." + "highlight1": "Família OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible tant a OpenAI Codex com a OpenAI Native.", + "highlight2": "Suport per a Grok 4.5 — el nou model insígnia de xAI, més una correcció del reasoning effort que també beneficia Grok 4 Mini.", + "highlight3": "Proveïdor Kenari — un gateway d'IA de primera classe compatible amb OpenAI, facturat en rupies, que cobreix Claude, GPT, DeepSeek, GLM, Kimi i més." }, "cloudAgents": { "heading": "Novetats al núvol:", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index be45daca7a..a9d3472229 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -341,9 +341,9 @@ "support": "Bitte unterstütze Zoo Code, indem du uns auf GitHub einen Stern gibst.", "release": { "heading": "Was ist neu:", - "highlight1": "Friendli-Anbieter mit GLM-5.2-Unterstützung — nutze das neueste GLM-Modell über Friendli.", - "highlight2": "Native Ollama-Unterstützung für thinking/reasoning — behalte die Reasoning-Ausgabe durchgängig bei, wenn du Ollama-Modelle verwendest.", - "highlight3": "Fix für benutzerdefinierte `apiModelId` bei Anthropic — benutzerdefinierte Anthropic-Modell-IDs bleiben jetzt ausgewählt, statt stillschweigend auf `claude-sonnet-4-5` zurückzufallen." + "highlight1": "OpenAI-GPT-5.6-Familie (Sol, Terra, Luna) — verfügbar über OpenAI Codex und OpenAI Native.", + "highlight2": "Grok-4.5-Unterstützung — xAIs neues Flaggschiff-Modell, plus ein Fix für den Reasoning Effort, von dem auch Grok 4 Mini profitiert.", + "highlight3": "Kenari-Anbieter — ein erstklassiges OpenAI-kompatibles KI-Gateway, das in Rupiah abgerechnet wird und Claude, GPT, DeepSeek, GLM, Kimi und mehr abdeckt." }, "cloudAgents": { "heading": "Neu in der Cloud:", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index f217fbf0b8..f3e2d85565 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -360,9 +360,9 @@ "support": "Please support Zoo Code by starring us on GitHub.", "release": { "heading": "What's New:", - "highlight1": "Friendli provider with GLM-5.2 support — use the latest GLM model through Friendli.", - "highlight2": "Native Ollama thinking/reasoning support — preserve reasoning output end-to-end when you use Ollama models.", - "highlight3": "Anthropic custom `apiModelId` fix — custom Anthropic model IDs now stay selected instead of silently falling back to `claude-sonnet-4-5`." + "highlight1": "OpenAI GPT-5.6 family (Sol, Terra, Luna) — available across both the OpenAI Codex and OpenAI Native provider paths.", + "highlight2": "Grok 4.5 support — xAI's new flagship model, plus a reasoning-effort fix that also benefits Grok 4 Mini.", + "highlight3": "Kenari provider — a first-class OpenAI-compatible AI gateway billed in Rupiah, covering Claude, GPT, DeepSeek, GLM, Kimi and more." }, "cloudAgents": { "heading": "New in the Cloud:", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 2e09e6cc78..9111e34430 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -341,9 +341,9 @@ "support": "Apoya a Zoo Code dándonos una estrella en GitHub.", "release": { "heading": "Novedades:", - "highlight1": "Proveedor Friendli con soporte para GLM-5.2 — usa el modelo GLM más reciente a través de Friendli.", - "highlight2": "Compatibilidad nativa de thinking/reasoning para Ollama — conserva la salida de razonamiento de extremo a extremo cuando uses modelos de Ollama.", - "highlight3": "Corrección de `apiModelId` personalizado de Anthropic — los IDs de modelo personalizados de Anthropic ahora permanecen seleccionados en lugar de volver silenciosamente a `claude-sonnet-4-5`." + "highlight1": "Familia OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible tanto en OpenAI Codex como en OpenAI Native.", + "highlight2": "Compatibilidad con Grok 4.5 — el nuevo modelo insignia de xAI, además de una corrección del reasoning effort que también beneficia a Grok 4 Mini.", + "highlight3": "Proveedor Kenari — un gateway de IA de primera línea compatible con OpenAI, facturado en rupias, que cubre Claude, GPT, DeepSeek, GLM, Kimi y más." }, "cloudAgents": { "heading": "Novedades en la Nube:", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 363977ddce..3238fcb101 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -341,9 +341,9 @@ "support": "Soutiens Zoo Code en nous donnant une étoile sur GitHub.", "release": { "heading": "Nouveautés :", - "highlight1": "Provider Friendli avec prise en charge de GLM-5.2 — utilise le dernier modèle GLM via Friendli.", - "highlight2": "Prise en charge native du thinking/reasoning pour Ollama — préserve la sortie de raisonnement de bout en bout quand tu utilises des modèles Ollama.", - "highlight3": "Correction du `apiModelId` personnalisé d'Anthropic — les identifiants de modèle Anthropic personnalisés restent désormais sélectionnés au lieu de revenir silencieusement à `claude-sonnet-4-5`." + "highlight1": "Famille OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible à la fois sur OpenAI Codex et OpenAI Native.", + "highlight2": "Prise en charge de Grok 4.5 — le nouveau modèle phare de xAI, avec une correction du reasoning effort qui profite aussi à Grok 4 Mini.", + "highlight3": "Provider Kenari — un gateway IA de premier ordre compatible OpenAI, facturé en roupies, couvrant Claude, GPT, DeepSeek, GLM, Kimi et plus." }, "cloudAgents": { "heading": "Nouveautés dans le Cloud :", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 39f74b6309..a1a0ed56a2 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -341,9 +341,9 @@ "support": "कृपया GitHub पर हमें स्टार देकर Zoo Code का समर्थन करें।", "release": { "heading": "नया क्या है:", - "highlight1": "GLM-5.2 समर्थन के साथ Friendli प्रदाता — Friendli के जरिए नवीनतम GLM मॉडल का उपयोग करें।", - "highlight2": "Ollama के लिए नेटिव thinking/reasoning समर्थन — Ollama मॉडल इस्तेमाल करते समय reasoning आउटपुट को शुरू से अंत तक सुरक्षित रखें।", - "highlight3": "Anthropic कस्टम `apiModelId` फिक्स — कस्टम Anthropic मॉडल ID अब चुपचाप `claude-sonnet-4-5` पर वापस जाने के बजाय चुनी हुई रहती हैं।" + "highlight1": "OpenAI GPT-5.6 परिवार (Sol, Terra, Luna) — OpenAI Codex और OpenAI Native दोनों में उपलब्ध।", + "highlight2": "Grok 4.5 समर्थन — xAI का नया flagship model, साथ ही reasoning effort की एक फिक्स जिसका फायदा Grok 4 Mini को भी मिलता है।", + "highlight3": "Kenari provider — एक first-class OpenAI-compatible AI gateway जो Rupiah में बिल होता है और Claude, GPT, DeepSeek, GLM, Kimi और अधिक को cover करता है।" }, "cloudAgents": { "heading": "क्लाउड में नया:", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 6eb35738f3..f0e73462e9 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -370,9 +370,9 @@ "support": "Dukung Zoo Code dengan memberi kami bintang di GitHub.", "release": { "heading": "Yang Baru:", - "highlight1": "Provider Friendli dengan dukungan GLM-5.2 — gunakan model GLM terbaru lewat Friendli.", - "highlight2": "Dukungan thinking/reasoning native untuk Ollama — pertahankan keluaran penalaran dari awal hingga akhir saat kamu memakai model Ollama.", - "highlight3": "Perbaikan `apiModelId` kustom Anthropic — ID model Anthropic kustom kini tetap terpilih alih-alih diam-diam kembali ke `claude-sonnet-4-5`." + "highlight1": "Keluarga OpenAI GPT-5.6 (Sol, Terra, Luna) — tersedia di OpenAI Codex maupun OpenAI Native.", + "highlight2": "Dukungan Grok 4.5 — model andalan baru xAI, plus perbaikan reasoning effort yang juga menguntungkan Grok 4 Mini.", + "highlight3": "Provider Kenari — gateway AI kelas utama yang kompatibel dengan OpenAI, ditagih dalam Rupiah, mencakup Claude, GPT, DeepSeek, GLM, Kimi, dan lainnya." }, "cloudAgents": { "heading": "Baru di Cloud:", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index caa84b54c0..2056e8d977 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -341,9 +341,9 @@ "support": "Sostieni Zoo Code lasciandoci una stella su GitHub.", "release": { "heading": "Novità:", - "highlight1": "Provider Friendli con supporto GLM-5.2 — usa il modello GLM più recente tramite Friendli.", - "highlight2": "Supporto nativo thinking/reasoning per Ollama — conserva l'output di ragionamento end-to-end quando usi i modelli Ollama.", - "highlight3": "Correzione del `apiModelId` personalizzato di Anthropic — gli ID modello Anthropic personalizzati ora restano selezionati invece di tornare silenziosamente a `claude-sonnet-4-5`." + "highlight1": "Famiglia OpenAI GPT-5.6 (Sol, Terra, Luna) — disponibile sia su OpenAI Codex sia su OpenAI Native.", + "highlight2": "Supporto per Grok 4.5 — il nuovo modello di punta di xAI, più una correzione del reasoning effort che avvantaggia anche Grok 4 Mini.", + "highlight3": "Provider Kenari — un gateway AI di prima classe compatibile con OpenAI, fatturato in rupie, che copre Claude, GPT, DeepSeek, GLM, Kimi e altro." }, "cloudAgents": { "heading": "Novità nel Cloud:", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 9a28dc9f37..88aa788626 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -341,9 +341,9 @@ "support": "GitHubでスターを付けて Zoo Code を応援してください。", "release": { "heading": "新機能:", - "highlight1": "GLM-5.2 をサポートした Friendli プロバイダー — Friendli 経由で最新の GLM モデルを使えます。", - "highlight2": "Ollama のネイティブ thinking/reasoning サポート — Ollama モデル使用時に reasoning 出力をエンドツーエンドで保持します。", - "highlight3": "Anthropic のカスタム `apiModelId` 修正 — カスタム Anthropic モデル ID が `claude-sonnet-4-5` に黙ってフォールバックせず、そのまま選択された状態を維持します。" + "highlight1": "OpenAI GPT-5.6 ファミリー(Sol、Terra、Luna)— OpenAI Codex と OpenAI Native の両方で利用可能。", + "highlight2": "Grok 4.5 サポート — xAI の新しいフラッグシップモデル。あわせて reasoning effort の修正も行われ、Grok 4 Mini にも恩恵があります。", + "highlight3": "Kenari プロバイダー — ルピー建てで請求される、Claude・GPT・DeepSeek・GLM・Kimi などに対応した OpenAI 互換のファーストクラス AI ゲートウェイ。" }, "cloudAgents": { "heading": "クラウドの新機能:", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 56b03165a7..d826bf7ca8 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -341,9 +341,9 @@ "support": "GitHub에서 별표를 눌러 Zoo Code를 응원해 주세요.", "release": { "heading": "새로운 기능:", - "highlight1": "GLM-5.2를 지원하는 Friendli 프로바이더 — Friendli를 통해 최신 GLM 모델을 사용할 수 있습니다.", - "highlight2": "Ollama용 네이티브 thinking/reasoning 지원 — Ollama 모델을 사용할 때 reasoning 출력을 처음부터 끝까지 유지합니다.", - "highlight3": "Anthropic 사용자 지정 `apiModelId` 수정 — 사용자 지정 Anthropic 모델 ID가 이제 조용히 `claude-sonnet-4-5`로 되돌아가지 않고 선택된 상태로 유지됩니다." + "highlight1": "OpenAI GPT-5.6 패밀리(Sol, Terra, Luna) — OpenAI Codex와 OpenAI Native 양쪽에서 사용 가능합니다.", + "highlight2": "Grok 4.5 지원 — xAI의 새로운 플래그십 모델이며, Grok 4 Mini에도 도움이 되는 reasoning effort 수정도 포함됩니다.", + "highlight3": "Kenari 프로바이더 — 루피아로 청구되며 Claude, GPT, DeepSeek, GLM, Kimi 등을 지원하는 OpenAI 호환 퍼스트클래스 AI 게이트웨이." }, "cloudAgents": { "heading": "클라우드의 새로운 기능:", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 357f5ca676..ad489050e7 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -314,9 +314,9 @@ "support": "Steun Zoo Code door ons een ster te geven op GitHub.", "release": { "heading": "Wat is nieuw:", - "highlight1": "Friendli-provider met GLM-5.2-ondersteuning — gebruik het nieuwste GLM-model via Friendli.", - "highlight2": "Native thinking/reasoning-ondersteuning voor Ollama — behoud reasoning-uitvoer van begin tot eind wanneer je Ollama-modellen gebruikt.", - "highlight3": "Fix voor aangepaste Anthropic-`apiModelId` — aangepaste Anthropic-model-ID's blijven nu geselecteerd in plaats van stilletjes terug te vallen op `claude-sonnet-4-5`." + "highlight1": "OpenAI GPT-5.6-familie (Sol, Terra, Luna) — beschikbaar via zowel OpenAI Codex als OpenAI Native.", + "highlight2": "Ondersteuning voor Grok 4.5 — xAI's nieuwe vlaggenschipmodel, plus een fix voor reasoning effort waar ook Grok 4 Mini baat bij heeft.", + "highlight3": "Kenari-provider — een eersteklas OpenAI-compatibele AI-gateway die in Rupiah wordt gefactureerd en Claude, GPT, DeepSeek, GLM, Kimi en meer ondersteunt." }, "cloudAgents": { "heading": "Nieuw in de Cloud:", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 0a04fe70ce..46da82306c 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -341,9 +341,9 @@ "support": "Wesprzyj Zoo Code, dając nam gwiazdkę na GitHub.", "release": { "heading": "Co nowego:", - "highlight1": "Provider Friendli z obsługą GLM-5.2 — używaj najnowszego modelu GLM przez Friendli.", - "highlight2": "Natywna obsługa thinking/reasoning dla Ollamy — zachowuj wynik rozumowania od początku do końca podczas korzystania z modeli Ollama.", - "highlight3": "Poprawka niestandardowego `apiModelId` Anthropic — niestandardowe identyfikatory modeli Anthropic pozostają teraz wybrane zamiast po cichu wracać do `claude-sonnet-4-5`." + "highlight1": "Rodzina OpenAI GPT-5.6 (Sol, Terra, Luna) — dostępna zarówno w OpenAI Codex, jak i OpenAI Native.", + "highlight2": "Obsługa Grok 4.5 — nowy flagowy model xAI, a także poprawka reasoning effort, z której korzysta również Grok 4 Mini.", + "highlight3": "Provider Kenari — wysokiej klasy gateway AI kompatybilny z OpenAI, rozliczany w rupii, obsługujący Claude, GPT, DeepSeek, GLM, Kimi i więcej." }, "cloudAgents": { "heading": "Nowości w chmurze:", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 2673c2aba0..83452930d6 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -341,9 +341,9 @@ "support": "Apoie o Zoo Code nos dando uma estrela no GitHub.", "release": { "heading": "Novidades:", - "highlight1": "Provedor Friendli com suporte a GLM-5.2 — use o modelo GLM mais recente pelo Friendli.", - "highlight2": "Suporte nativo de thinking/reasoning para Ollama — preserve a saída de raciocínio de ponta a ponta ao usar modelos do Ollama.", - "highlight3": "Correção de `apiModelId` personalizado da Anthropic — os IDs de modelo personalizados da Anthropic agora permanecem selecionados em vez de voltar silenciosamente para `claude-sonnet-4-5`." + "highlight1": "Família OpenAI GPT-5.6 (Sol, Terra, Luna) — disponível tanto no OpenAI Codex quanto no OpenAI Native.", + "highlight2": "Suporte ao Grok 4.5 — o novo modelo principal da xAI, além de uma correção no reasoning effort que também beneficia o Grok 4 Mini.", + "highlight3": "Provider Kenari — um gateway de IA de primeira classe compatível com OpenAI, cobrado em rúpias, cobrindo Claude, GPT, DeepSeek, GLM, Kimi e mais." }, "cloudAgents": { "heading": "Novidades na Nuvem:", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 74c69b2907..4e09e1e704 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -315,9 +315,9 @@ "support": "Поддержите Zoo Code, поставив нам звезду на GitHub.", "release": { "heading": "Что нового:", - "highlight1": "Провайдер Friendli с поддержкой GLM-5.2 — используй новейшую модель GLM через Friendli.", - "highlight2": "Нативная поддержка thinking/reasoning для Ollama — сохраняй вывод рассуждений от начала до конца при использовании моделей Ollama.", - "highlight3": "Исправление пользовательского `apiModelId` Anthropic — пользовательские идентификаторы моделей Anthropic теперь остаются выбранными вместо тихого возврата к `claude-sonnet-4-5`." + "highlight1": "Семейство OpenAI GPT-5.6 (Sol, Terra, Luna) — доступно как в OpenAI Codex, так и в OpenAI Native.", + "highlight2": "Поддержка Grok 4.5 — новая флагманская модель xAI, а также исправление reasoning effort, от которого выигрывает и Grok 4 Mini.", + "highlight3": "Провайдер Kenari — первоклассный AI-шлюз, совместимый с OpenAI, с выставлением счётов в рупиях, поддерживающий Claude, GPT, DeepSeek, GLM, Kimi и другие модели." }, "cloudAgents": { "heading": "Новое в облаке:", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index b1f0a248e2..88f6c25968 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -342,9 +342,9 @@ "support": "Lütfen GitHub'da bize yıldız vererek Zoo Code'u destekle.", "release": { "heading": "Yenilikler:", - "highlight1": "GLM-5.2 destekli Friendli sağlayıcısı — en yeni GLM modelini Friendli üzerinden kullan.", - "highlight2": "Ollama için yerel thinking/reasoning desteği — Ollama modellerini kullanırken reasoning çıktısını uçtan uca koru.", - "highlight3": "Anthropic özel `apiModelId` düzeltmesi — özel Anthropic model kimlikleri artık sessizce `claude-sonnet-4-5` modeline geri dönmek yerine seçili kalıyor." + "highlight1": "OpenAI GPT-5.6 ailesi (Sol, Terra, Luna) — hem OpenAI Codex hem de OpenAI Native üzerinde kullanılabilir.", + "highlight2": "Grok 4.5 desteği — xAI'nin yeni amiral gemisi modeli, ayrıca Grok 4 Mini'ye de fayda sağlayan bir reasoning effort düzeltmesi.", + "highlight3": "Kenari provider — Rupiah üzerinden faturalandırılan, Claude, GPT, DeepSeek, GLM, Kimi ve daha fazlasını kapsayan, OpenAI uyumlu birinci sınıf bir AI ağ geçidi." }, "cloudAgents": { "heading": "Cloud'daki yenilikler:", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index b9a0a36b4f..f506962395 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -342,9 +342,9 @@ "support": "Hãy ủng hộ Zoo Code bằng cách tặng sao cho chúng tôi trên GitHub.", "release": { "heading": "Có gì mới:", - "highlight1": "Nhà cung cấp Friendli với hỗ trợ GLM-5.2 — dùng mẫu GLM mới nhất qua Friendli.", - "highlight2": "Hỗ trợ thinking/reasoning gốc cho Ollama — giữ đầu ra suy luận xuyên suốt khi bạn dùng các mẫu Ollama.", - "highlight3": "Sửa lỗi `apiModelId` tùy chỉnh của Anthropic — ID mẫu Anthropic tùy chỉnh giờ sẽ tiếp tục được chọn thay vì âm thầm rơi về `claude-sonnet-4-5`." + "highlight1": "Dòng OpenAI GPT-5.6 (Sol, Terra, Luna) — có sẵn trên cả OpenAI Codex và OpenAI Native.", + "highlight2": "Hỗ trợ Grok 4.5 — mẫu chủ lực mới của xAI, cùng với bản sửa lỗi reasoning effort cũng có lợi cho Grok 4 Mini.", + "highlight3": "Provider Kenari — gateway AI hạng nhất tương thích OpenAI, tính phí bằng Rupiah, hỗ trợ Claude, GPT, DeepSeek, GLM, Kimi và nhiều hơn nữa." }, "cloudAgents": { "heading": "Mới trên Cloud:", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 504662b4d3..286e49c8f7 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -342,9 +342,9 @@ "support": "请在 GitHub 上为 Zoo Code 点星支持我们。", "release": { "heading": "新增功能:", - "highlight1": "支持 GLM-5.2 的 Friendli 提供商 — 通过 Friendli 使用最新的 GLM 模型。", - "highlight2": "Ollama 原生 thinking/reasoning 支持 — 使用 Ollama 模型时可端到端保留 reasoning 输出。", - "highlight3": "Anthropic 自定义 `apiModelId` 修复 — 自定义 Anthropic 模型 ID 现在会保持选中,不再静默回退到 `claude-sonnet-4-5`。" + "highlight1": "OpenAI GPT-5.6 系列(Sol、Terra、Luna)— 已在 OpenAI Codex 与 OpenAI Native 中同时提供。", + "highlight2": "支持 Grok 4.5 — xAI 全新旗舰模型,同时修复了 reasoning effort 问题,Grok 4 Mini 也因此受益。", + "highlight3": "Kenari 提供商 — 以卢比计费的一流 OpenAI 兼容 AI 网关,支持 Claude、GPT、DeepSeek、GLM、Kimi 等。" }, "cloudAgents": { "heading": "云端新功能:", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 9d31c51e9f..6b6aeba6fe 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -363,9 +363,9 @@ "support": "請在 GitHub 上為 Zoo Code 加星支持我們。", "release": { "heading": "新增功能:", - "highlight1": "支援 GLM-5.2 的 Friendli 供應商 — 透過 Friendli 使用最新的 GLM 模型。", - "highlight2": "Ollama 原生 thinking/reasoning 支援 — 使用 Ollama 模型時可端對端保留 reasoning 輸出。", - "highlight3": "Anthropic 自訂 `apiModelId` 修正 — 自訂 Anthropic 模型 ID 現在會保持選取,不再靜默回退到 `claude-sonnet-4-5`。" + "highlight1": "OpenAI GPT-5.6 系列(Sol、Terra、Luna)— 已在 OpenAI Codex 與 OpenAI Native 中同時提供。", + "highlight2": "支援 Grok 4.5 — xAI 全新旗艦模型,同時修正了 reasoning effort 問題,Grok 4 Mini 也因此受惠。", + "highlight3": "Kenari 供應商 — 以盧比計費的一流 OpenAI 相容 AI 閘道,支援 Claude、GPT、DeepSeek、GLM、Kimi 等。" }, "cloudAgents": { "heading": "雲端的新功能:", From 5f582a512cfd7cc219302225f0673d78c3b57d03 Mon Sep 17 00:00:00 2001 From: eason liang <136036952+easonLiangWorldedtech@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:47:19 +0800 Subject: [PATCH 33/89] feat(api): add CompletePromptOptions parameter to completePrompt method (#901) * feat(api): add CompletePromptOptions parameter to completePrompt method Add optional second parameter to all provider completePrompt methods for future abort signal and timeout support. - Add CompletePromptOptions interface with abortSignal and timeoutMs fields - Update 25+ provider implementations to accept options?: CompletePromptOptions parameter - Simplify providers to use basic request calls without signal/timeout logic * test(fake-ai): add test coverage for countTokens and completePrompt delegation * refactor(Task.spec): simplify mockStreamFactory to standard async generator * fix: CompletePromptOptions naming align Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> * fix: remove inline import in providers\openai.ts Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --------- Co-authored-by: Eason Liang Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- src/api/index.ts | 11 ++- src/api/providers/__tests__/fake-ai.spec.ts | 79 ++++++++++++++++++ src/api/providers/anthropic-vertex.ts | 4 +- src/api/providers/anthropic.ts | 4 +- .../base-openai-compatible-provider.ts | 4 +- src/api/providers/bedrock.ts | 4 +- src/api/providers/fake-ai.ts | 13 ++- src/api/providers/gemini.ts | 4 +- src/api/providers/kenari.ts | 4 +- src/api/providers/lite-llm.ts | 4 +- src/api/providers/lm-studio.ts | 4 +- src/api/providers/minimax.ts | 4 +- src/api/providers/mistral.ts | 5 +- src/api/providers/native-ollama.ts | 4 +- src/api/providers/openai-codex.ts | 4 +- src/api/providers/openai-compatible.ts | 4 +- src/api/providers/openai-native.ts | 12 ++- src/api/providers/openai.ts | 4 +- src/api/providers/opencode-go.ts | 4 +- src/api/providers/openrouter.ts | 4 +- src/api/providers/poe.ts | 4 +- src/api/providers/qwen-code.ts | 4 +- src/api/providers/requesty.ts | 4 +- src/api/providers/unbound.ts | 4 +- src/api/providers/vercel-ai-gateway.ts | 4 +- src/api/providers/vscode-lm.ts | 4 +- src/api/providers/xai.ts | 4 +- src/api/providers/zoo-gateway.ts | 4 +- src/core/task/__tests__/Task.spec.ts | 82 ++++++++++++++++++- src/utils/__tests__/enhance-prompt.spec.ts | 4 +- src/utils/single-completion-handler.ts | 10 ++- 31 files changed, 238 insertions(+), 70 deletions(-) create mode 100644 src/api/providers/__tests__/fake-ai.spec.ts diff --git a/src/api/index.ts b/src/api/index.ts index 5a9532b43d..48a070feb4 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -42,8 +42,17 @@ import { } from "./providers" import { NativeOllamaHandler } from "./providers/native-ollama" +/** + * Options for completePrompt — unified with ApiHandlerCreateMessageMetadata. + * Uses abortSignal (not signal) to match the metadata pattern used in stream path. + */ +export interface CompletePromptOptions extends Pick { + /** Optional timeout override (ms) — falls back to provider default if omitted */ + timeoutMs?: number +} + export interface SingleCompletionHandler { - completePrompt(prompt: string): Promise + completePrompt(prompt: string, options?: CompletePromptOptions): Promise } export interface ApiHandlerCreateMessageMetadata { diff --git a/src/api/providers/__tests__/fake-ai.spec.ts b/src/api/providers/__tests__/fake-ai.spec.ts new file mode 100644 index 0000000000..955264c68f --- /dev/null +++ b/src/api/providers/__tests__/fake-ai.spec.ts @@ -0,0 +1,79 @@ +import type { Anthropic } from "@anthropic-ai/sdk" + +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../../index" +import { FakeAIHandler } from "../fake-ai" + +const modelInfo = { + contextWindow: 8192, + maxTokens: 4096, + supportsImages: false, + supportsPromptCache: false, +} + +describe("FakeAIHandler", () => { + it("should delegate completePrompt with options to the cached FakeAI instance", async () => { + const completePrompt = vitest.fn().mockResolvedValue("delegated response") + const fakeAi: { + id: string + createMessage: () => AsyncGenerator + getModel: () => { id: string; info: typeof modelInfo } + countTokens: ReturnType + completePrompt: typeof completePrompt + removeFromCache?: () => void + } = { + id: "fake-ai-completePrompt-delegation", + createMessage: async function* () {}, + getModel: () => ({ id: "fake-model", info: modelInfo }), + countTokens: vitest.fn().mockResolvedValue(0), + completePrompt, + } + const controller = new AbortController() + const options: CompletePromptOptions = { abortSignal: controller.signal, timeoutMs: 1234 } + + const handler = new FakeAIHandler({ fakeAi }) + const result = await handler.completePrompt("Test prompt", options) + + expect(result).toBe("delegated response") + expect(completePrompt).toHaveBeenCalledWith("Test prompt", options) + fakeAi.removeFromCache?.() + }) + + it("should delegate createMessage, getModel, and countTokens to FakeAI", async () => { + const metadata = { taskId: "task-1" } as ApiHandlerCreateMessageMetadata + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }] + const createMessage = vitest.fn(async function* () { + yield { type: "text" as const, text: "Hello" } + }) + const getModel = vitest.fn(() => ({ id: "fake-model", info: modelInfo })) + const countTokens = vitest.fn().mockResolvedValue(7) + const fakeAi: { + id: string + createMessage: typeof createMessage + getModel: typeof getModel + countTokens: typeof countTokens + completePrompt: ReturnType + removeFromCache?: () => void + } = { + id: "fake-ai-handler-delegation", + createMessage, + getModel, + countTokens, + completePrompt: vitest.fn().mockResolvedValue("complete"), + } + + const handler = new FakeAIHandler({ fakeAi }) + const chunks = [] + for await (const chunk of handler.createMessage("System", messages, metadata)) { + chunks.push(chunk) + } + + expect(chunks).toEqual([{ type: "text", text: "Hello" }]) + expect(createMessage).toHaveBeenCalledWith("System", messages, metadata) + expect(handler.getModel()).toEqual({ id: "fake-model", info: modelInfo }) + expect(getModel).toHaveBeenCalledTimes(1) + await expect(handler.countTokens(content)).resolves.toBe(7) + expect(countTokens).toHaveBeenCalledWith(content) + fakeAi.removeFromCache?.() + }) +}) diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index 4f62fe0b1e..9d91b25fe5 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/src/api/providers/anthropic-vertex.ts @@ -25,7 +25,7 @@ import { import { BaseProvider } from "./base-provider" import { parseVertexJsonCredentials } from "./utils/vertex-credentials" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" // https://docs.anthropic.com/en/api/claude-on-vertex-ai export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler { @@ -270,7 +270,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple } } - async completePrompt(prompt: string) { + async completePrompt(prompt: string, options?: CompletePromptOptions) { try { const { id, diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 2728dae1ad..f69c6eb29c 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -22,7 +22,7 @@ import { getAnthropicProviderReasoning } from "../transform/reasoning" import { handleProviderError } from "./utils/error-handler" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { calculateApiCostAnthropic } from "../../shared/cost" import { convertOpenAIToolsToAnthropic, @@ -429,7 +429,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } } - async completePrompt(prompt: string) { + async completePrompt(prompt: string, options?: CompletePromptOptions) { const { id: model, temperature } = this.getModel() let message diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index d6406f2a67..f4928b0b0a 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -8,7 +8,7 @@ import { TagMatcher } from "../../utils/tag-matcher" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import { handleOpenAIError } from "./utils/error-handler" @@ -212,7 +212,7 @@ export abstract class BaseOpenAiCompatibleProvider } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: modelId, info: modelInfo } = this.getModel() const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 8c2b5ace68..ae072b3559 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -44,7 +44,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor import { getModelParams } from "../transform/model-params" import { shouldUseReasoningBudget } from "../../shared/api" import { normalizeToolSchema } from "../../utils/json-schema" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" /************************************************************************************ * @@ -798,7 +798,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { const modelConfig = this.getModel() diff --git a/src/api/providers/fake-ai.ts b/src/api/providers/fake-ai.ts index e69a1c84e8..f6c466b454 100644 --- a/src/api/providers/fake-ai.ts +++ b/src/api/providers/fake-ai.ts @@ -2,7 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk" import type { ModelInfo } from "@roo-code/types" -import type { ApiHandler, SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { + ApiHandler, + SingleCompletionHandler, + ApiHandlerCreateMessageMetadata, + CompletePromptOptions, +} from "../index" import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" @@ -28,7 +33,7 @@ interface FakeAI { ): ApiStream getModel(): { id: string; info: ModelInfo } countTokens(content: Array): Promise - completePrompt(prompt: string): Promise + completePrompt(prompt: string, options?: CompletePromptOptions): Promise } /** @@ -75,7 +80,7 @@ export class FakeAIHandler implements ApiHandler, SingleCompletionHandler { return this.ai.countTokens(content) } - completePrompt(prompt: string): Promise { - return this.ai.completePrompt(prompt) + completePrompt(prompt: string, options?: CompletePromptOptions): Promise { + return this.ai.completePrompt(prompt, options) } } diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 6c8168cae2..44065d8801 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -23,7 +23,7 @@ import { t } from "i18next" import type { ApiStream, GroundingSource } from "../transform/stream" import { getModelParams } from "../transform/model-params" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { BaseProvider } from "./base-provider" import { parseVertexJsonCredentials } from "./utils/vertex-credentials" @@ -576,7 +576,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl return citationLinks.join(", ") } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: model, info } = this.getModel() try { diff --git a/src/api/providers/kenari.ts b/src/api/providers/kenari.ts index 75c02ff164..7895a9452c 100644 --- a/src/api/providers/kenari.ts +++ b/src/api/providers/kenari.ts @@ -13,7 +13,7 @@ import { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { RouterProvider } from "./router-provider" import { extractReasoningFromDelta } from "./utils/extract-reasoning" @@ -123,7 +123,7 @@ export class KenariHandler extends RouterProvider implements SingleCompletionHan * @returns The model's reply text, or an empty string if no content is returned. * @throws Error with a Kenari-specific prefix if the request fails. */ - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: modelId, info } = await this.fetchModel() try { diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index 3f9b3732e5..49cc1b115b 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -12,7 +12,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import { GEMINI_THOUGHT_SIGNATURE_BYPASS } from "../transform/gemini-format" import { sanitizeOpenAiCallId } from "../../utils/tool-id" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { RouterProvider } from "./router-provider" import { extractReasoningFromDelta } from "./utils/extract-reasoning" @@ -311,7 +311,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: modelId, info } = await this.fetchModel() // Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index 891695b5fc..39216418db 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -13,7 +13,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { getModelsFromCache } from "./fetchers/modelCache" import { handleOpenAIError } from "./utils/error-handler" @@ -187,7 +187,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { // Create params object with optional draft model const params: any = { diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index 93aa7ea8f1..e209add72d 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -12,7 +12,7 @@ import { getModelParams } from "../transform/model-params" import { mergeEnvironmentDetailsForMiniMax } from "../transform/minimax-format" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { calculateApiCostAnthropic } from "../../shared/cost" import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters" @@ -289,7 +289,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand } } - async completePrompt(prompt: string) { + async completePrompt(prompt: string, options?: CompletePromptOptions) { const { id: model, temperature } = this.getModel() const message = await this.client.messages.create({ diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index e0e19298f4..c7816feaa2 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -18,7 +18,7 @@ import { ApiStream } from "../transform/stream" import { handleProviderError } from "./utils/error-handler" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" // Type helper to handle thinking chunks from Mistral API // The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union @@ -192,8 +192,7 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand return { id, info, maxTokens, temperature } } - - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: model, temperature } = this.getModel() try { diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts index 2a14885e11..eb380d1eb2 100644 --- a/src/api/providers/native-ollama.ts +++ b/src/api/providers/native-ollama.ts @@ -7,7 +7,7 @@ import { BaseProvider } from "./base-provider" import type { ApiHandlerOptions } from "../../shared/api" import { getOllamaModels } from "./fetchers/ollama" import { TagMatcher } from "../../utils/tag-matcher" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" interface OllamaChatOptions { temperature: number @@ -550,7 +550,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { const client = this.ensureClient() const { id: modelId } = await this.fetchModel() diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 2b2a599c0f..374a715b34 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -21,7 +21,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { isMcpTool } from "../../utils/mcp-name" import { sanitizeOpenAiCallId } from "../../utils/tool-id" import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth" @@ -1154,7 +1154,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion return this.lastResponseId } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { this.abortController = new AbortController() try { diff --git a/src/api/providers/openai-compatible.ts b/src/api/providers/openai-compatible.ts index d129e72452..308652804a 100644 --- a/src/api/providers/openai-compatible.ts +++ b/src/api/providers/openai-compatible.ts @@ -17,7 +17,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" /** * Configuration options for creating an OpenAI-compatible provider. @@ -197,7 +197,7 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si /** * Complete a prompt using the AI SDK generateText. */ - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const languageModel = this.getLanguageModel() const { text } = await generateText({ diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 8f86f46751..a1ce2d89d0 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -26,7 +26,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { isMcpTool } from "../../utils/mcp-name" import { sanitizeOpenAiCallId } from "../../utils/tool-id" @@ -1484,14 +1484,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio getResponseId(): string | undefined { return this.lastResponseId } - - async completePrompt(prompt: string): Promise { - // Create AbortController for cancellation - this.abortController = new AbortController() - + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { + this.abortController = new AbortController() + const model = this.getModel() - const { verbosity, reasoning } = model + const { verbosity } = model // Resolve reasoning effort for models that support it const reasoningEffort = this.getReasoningEffort(model) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 07427c7dbe..ca0305aab7 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -21,7 +21,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { handleOpenAIError } from "./utils/error-handler" import { extractReasoningFromDelta } from "./utils/extract-reasoning" @@ -296,7 +296,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl return { id, info, ...params } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { const isAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const model = this.getModel() diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index 27d8ab3f7e..be53dc1c02 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -18,7 +18,7 @@ import { convertToR1Format } from "../transform/r1-format" import { filterNonAnthropicBlocks } from "../transform/anthropic-filter" import { getModelParams } from "../transform/model-params" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { RouterProvider } from "./router-provider" import { extractReasoningFromDelta } from "./utils/extract-reasoning" import { DEFAULT_HEADERS } from "./constants" @@ -485,7 +485,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio * @returns The model's reply text, or an empty string if no content is returned. * @throws Error with an Opencode Go-specific prefix if the request fails. */ - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: modelId, format, temperature, reasoningEffort, maxTokens } = await this.resolveModel() if (format === "anthropic") { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 360d4c7f81..3e59b4360b 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -35,7 +35,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions, SingleCompletionHandler } from "../index" import { handleOpenAIError } from "./utils/error-handler" import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" @@ -574,7 +574,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH return { id, info, topP: isDeepSeekR1 ? 0.95 : undefined, ...params } } - async completePrompt(prompt: string) { + async completePrompt(prompt: string, options?: CompletePromptOptions) { const { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel() const completionParams: OpenRouterChatCompletionParams = { diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index 9513a1445d..1e5315b1ba 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -18,7 +18,7 @@ import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { getModelsFromCache } from "./fetchers/modelCache" const DEFAULT_THINKING_BUDGET = 8192 @@ -138,7 +138,7 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id } = this.getModel() try { const { text } = await generateText({ diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts index cdf0f88e4b..5001b4c8ed 100644 --- a/src/api/providers/qwen-code.ts +++ b/src/api/providers/qwen-code.ts @@ -15,7 +15,7 @@ import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import { extractReasoningFromDelta } from "./utils/extract-reasoning" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai" const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token` @@ -327,7 +327,7 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan return { id, info } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { await this.ensureAuthenticated() const client = this.ensureClient() const model = this.getModel() diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 006cd9cdfb..5753660de5 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -14,7 +14,7 @@ import { AnthropicProviderReasoningParams, getAnthropicProviderReasoning } from import { DEFAULT_HEADERS } from "./constants" import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { toRequestyServiceUrl } from "../../shared/utils/requesty" import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" @@ -204,7 +204,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel() const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }] diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index cb41591634..c3ec9c44fc 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -14,7 +14,7 @@ import { OpenAiReasoningParams } from "../transform/reasoning" import { DEFAULT_HEADERS } from "./constants" import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" import { extractReasoningFromDelta } from "./utils/extract-reasoning" @@ -192,7 +192,7 @@ export class UnboundHandler extends BaseProvider implements SingleCompletionHand } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel() const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }] diff --git a/src/api/providers/vercel-ai-gateway.ts b/src/api/providers/vercel-ai-gateway.ts index 0c7bd1d485..0820e1b2d8 100644 --- a/src/api/providers/vercel-ai-gateway.ts +++ b/src/api/providers/vercel-ai-gateway.ts @@ -14,7 +14,7 @@ import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { RouterProvider } from "./router-provider" // Extend OpenAI's CompletionUsage to include Vercel AI Gateway specific fields @@ -117,7 +117,7 @@ export class VercelAiGatewayHandler extends RouterProvider implements SingleComp } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const { id: modelId, info } = await this.fetchModel() try { diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 5b9064363f..5c88a947de 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -12,7 +12,7 @@ import { ApiStream } from "../transform/stream" import { convertToVsCodeLmMessages, extractTextCountFromMessage } from "../transform/vscode-lm-format" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" /** * Converts OpenAI-format tools to VSCode Language Model tools. @@ -582,7 +582,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan return this.getModel().info.contextWindow } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { try { const client = await this.getClient() const response = await client.sendRequest( diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 15753c0043..c47e9cefd1 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -13,7 +13,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { handleOpenAIError } from "./utils/error-handler" import { isMcpTool } from "../../utils/mcp-name" @@ -144,7 +144,7 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler yield* processResponsesApiStream(stream, normalizeUsage) } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { const model = this.getModel() try { diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index 4724464ff3..cea410bb2e 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -18,7 +18,7 @@ import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" -import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { RouterProvider } from "./router-provider" function getApiErrorStatus(error: unknown): number | undefined { @@ -276,7 +276,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { this.ensureAuthenticated() const { id: modelId, info } = await this.fetchModel() diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 27ba5ce8ff..0c784821c1 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1812,7 +1812,7 @@ describe("Cline", () => { expect(summarizeConversation).toHaveBeenCalled() const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! - expect(options.metadata?.abortSignal).toBeInstanceOf(AbortSignal) + expect(options.metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("should omit abortSignal from condenseContext metadata when no current request exists", async () => { @@ -1899,7 +1899,7 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() - expect(metadata!.abortSignal).toBeInstanceOf(AbortSignal) + expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { @@ -2136,7 +2136,7 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() expect("abortSignal" in metadata!).toBe(true) - expect(metadata!.abortSignal).toBeInstanceOf(AbortSignal) + expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("should keep createMessage abortSignal metadata unaborted before cancellation", async () => { @@ -2197,11 +2197,85 @@ describe("Cline", () => { await iterator.next() const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata?.abortSignal).toBeInstanceOf(AbortSignal) + expect(metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) expect(metadata?.abortSignal?.aborted).toBe(false) }) }) + it("should create a fresh AbortController for each sequential request", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + let callCount = 0 + const mockStreamFactory = async function* (): AsyncGenerator { + yield { type: "text", text: `response ${callCount}` } + callCount++ + } + const createMessageSpy = vi + .spyOn(task.api, "createMessage") + .mockImplementation(() => mockStreamFactory()) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + // First request + const iterator1 = task.attemptApiRequest(0) + await iterator1.next() + + expect(createMessageSpy).toHaveBeenCalledTimes(1) + const [, , metadata1] = createMessageSpy.mock.calls[0]! + const signal1 = metadata1!.abortSignal + expect(signal1).toBeDefined() + expect(signal1!.aborted).toBe(false) + + // Simulate request completion and cancellation to clear the controller + task.cancelCurrentRequest() + + // Second request should create a fresh AbortController with a new signal + callCount = 0 + const iterator2 = task.attemptApiRequest(0) + await iterator2.next() + + expect(createMessageSpy).toHaveBeenCalledTimes(2) + const [, , metadata2] = createMessageSpy.mock.calls[1]! + const signal2 = metadata2!.abortSignal + + // Signals should be different instances (fresh controller per request) + expect(signal2).not.toBe(signal1) + expect(signal2).toBe(task.currentRequestAbortController!.signal) + expect(signal2!.aborted).toBe(false) + }) + it("should propagate AbortController signal through attemptApiRequest context-window retry path", async () => { const task = new Task({ provider: mockProvider, diff --git a/src/utils/__tests__/enhance-prompt.spec.ts b/src/utils/__tests__/enhance-prompt.spec.ts index 2546878d8c..7e8c702984 100644 --- a/src/utils/__tests__/enhance-prompt.spec.ts +++ b/src/utils/__tests__/enhance-prompt.spec.ts @@ -42,7 +42,7 @@ describe("enhancePrompt", () => { expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) - expect((handler as any).completePrompt).toHaveBeenCalledWith(`Test prompt`) + expect((handler as any).completePrompt).toHaveBeenCalledWith(`Test prompt`, undefined) }) it("enhances prompt using custom enhancement prompt when provided", async () => { @@ -64,7 +64,7 @@ describe("enhancePrompt", () => { expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) - expect((handler as any).completePrompt).toHaveBeenCalledWith(`${customEnhancePrompt}\n\nTest prompt`) + expect((handler as any).completePrompt).toHaveBeenCalledWith(`${customEnhancePrompt}\n\nTest prompt`, undefined) }) it("throws error for empty prompt input", async () => { diff --git a/src/utils/single-completion-handler.ts b/src/utils/single-completion-handler.ts index 4606a17bab..4890b38471 100644 --- a/src/utils/single-completion-handler.ts +++ b/src/utils/single-completion-handler.ts @@ -1,12 +1,16 @@ import type { ProviderSettings } from "@roo-code/types" -import { buildApiHandler, SingleCompletionHandler } from "../api" +import { buildApiHandler, SingleCompletionHandler, type CompletePromptOptions } from "../api" /** * Enhances a prompt using the configured API without creating a full Cline instance or task history. * This is a lightweight alternative that only uses the API's completion functionality. */ -export async function singleCompletionHandler(apiConfiguration: ProviderSettings, promptText: string): Promise { +export async function singleCompletionHandler( + apiConfiguration: ProviderSettings, + promptText: string, + options?: CompletePromptOptions, +): Promise { if (!promptText) { throw new Error("No prompt text provided") } @@ -21,5 +25,5 @@ export async function singleCompletionHandler(apiConfiguration: ProviderSettings throw new Error("The selected API provider does not support prompt enhancement") } - return (handler as SingleCompletionHandler).completePrompt(promptText) + return (handler as SingleCompletionHandler).completePrompt(promptText, options) } From c322f3c80f22a2064a4c4248e5876cfd303860b4 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:17:03 +0000 Subject: [PATCH 34/89] ci(code-qa): upload coverage lcov reports to GitHub artifacts (#939) Co-authored-by: Roomote --- .github/workflows/code-qa.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 6dc7b33d9d..f0e7ff0aa7 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -170,3 +170,17 @@ jobs: disable_search: true flags: ${{ matrix.codecov-flag }},core-integration token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload coverage reports to GitHub + if: matrix.upload-coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: coverage-reports-${{ matrix.name }} + path: | + src/coverage/lcov.info + webview-ui/coverage/lcov.info + packages/cloud/coverage/lcov.info + packages/telemetry/coverage/lcov.info + apps/cli/coverage/lcov.info + packages/core/coverage/unit/lcov.info + packages/core/coverage/integration/lcov.info + retention-days: 7 From 6e05ae951db1a0493d883c8cf7fb735d1b5a97e8 Mon Sep 17 00:00:00 2001 From: Morgan Carr <256248948+morgan-coded@users.noreply.github.com> Date: Sun, 19 Jul 2026 08:40:25 -0500 Subject: [PATCH 35/89] fix(task-history): route invalidate() and invalidateAll() through withLock (#912) * fix: lock task history invalidation * fix(task-history): address invalidation review feedback --- src/core/task-persistence/TaskHistoryStore.ts | 26 +++-- .../__tests__/TaskHistoryStore.spec.ts | 101 ++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 2 +- 3 files changed, 117 insertions(+), 12 deletions(-) diff --git a/src/core/task-persistence/TaskHistoryStore.ts b/src/core/task-persistence/TaskHistoryStore.ts index 219918b029..c6c3c6910f 100644 --- a/src/core/task-persistence/TaskHistoryStore.ts +++ b/src/core/task-persistence/TaskHistoryStore.ts @@ -430,23 +430,27 @@ export class TaskHistoryStore { * Invalidate a single task's cache entry (re-read from disk on next access). */ async invalidate(taskId: string): Promise { - try { - const item = await this.readTaskFile(taskId) - if (item) { - this.cache.set(taskId, item) - } else { + return this.withLock(async () => { + try { + const item = await this.readTaskFile(taskId) + if (item) { + this.cache.set(taskId, item) + } else { + this.cache.delete(taskId) + } + } catch { this.cache.delete(taskId) } - } catch { - this.cache.delete(taskId) - } + }) } /** - * Clear all in-memory cache and reload from index. + * Clear all in-memory cache entries; a subsequent `reconcile()` repopulates them from task files. */ - invalidateAll(): void { - this.cache.clear() + async invalidateAll(): Promise { + return this.withLock(async () => { + this.cache.clear() + }) } // ────────────────────────────── Migration ────────────────────────────── diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts index 75e0a16253..3b7e9041a4 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts @@ -440,6 +440,107 @@ describe("TaskHistoryStore", () => { expect(store.get("gone-task")).toBeUndefined() }) + + it("waits for an in-flight write before refreshing the cache", async () => { + await store.initialize() + + const item = makeHistoryItem({ id: "invalidate-locked", tokensIn: 100 }) + await store.upsert(item) + + let signalWriteStarted!: () => void + const writeStarted = new Promise((resolve) => { + signalWriteStarted = resolve + }) + let releaseWrite!: () => void + const writeCanFinish = new Promise((resolve) => { + releaseWrite = resolve + }) + let releaseStaleRead!: () => void + const staleReadCanFinish = new Promise((resolve) => { + releaseStaleRead = resolve + }) + let writeReleased = false + + const storeAny = store as any + const originalWriteTaskFile = storeAny.writeTaskFile.bind(store) + const originalReadTaskFile = storeAny.readTaskFile.bind(store) + vi.spyOn(storeAny, "writeTaskFile").mockImplementation(async (...args: unknown[]) => { + const next = args[0] as HistoryItem + if (next.id === item.id && next.tokensIn === 999) { + signalWriteStarted() + await writeCanFinish + } + return originalWriteTaskFile(...args) + }) + vi.spyOn(storeAny, "readTaskFile").mockImplementation(async (...args: unknown[]) => { + if (args[0] === item.id && !writeReleased) { + await staleReadCanFinish + return item + } + return originalReadTaskFile(...args) + }) + + const write = store.upsert({ ...item, tokensIn: 999 }) + await writeStarted + const invalidation = store.invalidate(item.id) + + writeReleased = true + releaseWrite() + await write + releaseStaleRead() + await invalidation + + expect(store.get(item.id)?.tokensIn).toBe(999) + }) + }) + + describe("invalidateAll()", () => { + it("waits for an in-flight write before clearing the cache", async () => { + const onWrite = vi.fn().mockResolvedValue(undefined) + store = new TaskHistoryStore(tmpDir, { onWrite }) + await store.initialize() + + const first = makeHistoryItem({ id: "invalidate-all-first", ts: 1000, tokensIn: 100 }) + const second = makeHistoryItem({ id: "invalidate-all-second", ts: 2000 }) + await store.upsert(first) + await store.upsert(second) + onWrite.mockClear() + + let signalWriteStarted!: () => void + const writeStarted = new Promise((resolve) => { + signalWriteStarted = resolve + }) + let releaseWrite!: () => void + const writeCanFinish = new Promise((resolve) => { + releaseWrite = resolve + }) + + const storeAny = store as any + const originalWriteTaskFile = storeAny.writeTaskFile.bind(store) + vi.spyOn(storeAny, "writeTaskFile").mockImplementation(async (...args: unknown[]) => { + const item = args[0] as HistoryItem + if (item.id === first.id && item.tokensIn === 999) { + signalWriteStarted() + await writeCanFinish + } + return originalWriteTaskFile(...args) + }) + + const write = store.upsert({ ...first, tokensIn: 999 }) + await writeStarted + const invalidation = store.invalidateAll() + + releaseWrite() + await write + await invalidation + + expect(onWrite).toHaveBeenCalledTimes(1) + expect(onWrite.mock.calls[0][0].map((item: HistoryItem) => item.id).sort()).toEqual([ + "invalidate-all-first", + "invalidate-all-second", + ]) + expect(store.getAll()).toEqual([]) + }) }) describe("atomicUpdatePair()", () => { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 01abc5db98..c3918e24a5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -963,7 +963,7 @@ export const webviewMessageHandler = async ( // Refresh history whenever Roo tasks were found — even if all already existed — // so a retry after a partial-copy failure still reconciles the store. - provider.taskHistoryStore.invalidateAll() + await provider.taskHistoryStore.invalidateAll() await provider.taskHistoryStore.reconcile() await provider.taskHistoryStore.flushIndex() await provider.postStateToWebview() From 8c17b41059b96cbf34aae042450265c805866fec Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:32:51 -0400 Subject: [PATCH 36/89] test(task-lifecycle): add regression coverage for issue #566 (interrupted subtask parent-link) (#911) --- apps/vscode-e2e/src/fixtures/subtasks.ts | 83 ++++++++++ apps/vscode-e2e/src/suite/subtasks.test.ts | 152 ++++++++++++++++++ .../history-resume-delegation.spec.ts | 145 +++++++++++++++++ 3 files changed, 380 insertions(+) diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index 772eb3306a..721af81630 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -7,6 +7,8 @@ const SUBTASK_PARENT_MARKER = "SUBTASK_PARENT_CANCELLATION_SMOKE" const SUBTASK_CHILD_MARKER = "SUBTASK_CHILD_CALCULATOR_SMOKE" const SUBTASK_INTERRUPT_PARENT_MARKER = "SUBTASK_PARENT_INTERRUPT_RESUME" const SUBTASK_INTERRUPT_CHILD_MARKER = "SUBTASK_CHILD_INTERRUPT_RESUME" +export const SUBTASK_API_HANG_PARENT_MARKER = "SUBTASK_PARENT_API_HANG_INTERRUPT_RESUME" +export const SUBTASK_API_HANG_CHILD_MARKER = "SUBTASK_CHILD_API_HANG_INTERRUPT_RESUME" const SUBTASK_FAST_PARENT_MARKER = "SUBTASK_PARENT_IMMEDIATE_COMPLETION" const SUBTASK_FAST_CHILD_MARKER = "SUBTASK_CHILD_IMMEDIATE_COMPLETION" const SUBTASK_XPROFILE_PARENT_MARKER = "SUBTASK_PARENT_CROSS_PROFILE" @@ -24,6 +26,12 @@ export const SUBTASK_INTERRUPT_PARENT_PROMPT = `${SUBTASK_INTERRUPT_PARENT_MARKE export const SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER = "9" export const SUBTASK_INTERRUPT_PARENT_RESULT = "Interrupted parent resumed" +const SUBTASK_API_HANG_CHILD_PROMPT = `${SUBTASK_API_HANG_CHILD_MARKER}: Complete with the exact result "Hung child completed".` +export const SUBTASK_API_HANG_PARENT_PROMPT = `${SUBTASK_API_HANG_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_API_HANG_CHILD_PROMPT}" Do not answer directly. When the subtask returns, complete with the exact result "API hang parent resumed".` +export const SUBTASK_API_HANG_RESUME_MESSAGE = "Continue after provider hang." +export const SUBTASK_API_HANG_CHILD_RESULT = "Hung child completed" +export const SUBTASK_API_HANG_PARENT_RESULT = "API hang parent resumed" + const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${SUBTASK_XPROFILE_SAME_CHILD_MARKER}: Complete immediately with the exact result "Same-profile child completed".` const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER}: Complete immediately with the exact result "Different-profile child completed".` export const SUBTASK_XPROFILE_PARENT_PROMPT = `${SUBTASK_XPROFILE_PARENT_MARKER}: First use new_task to create a code-mode subtask with this exact message: "${SUBTASK_XPROFILE_SAME_CHILD_PROMPT}" After it returns, create an ask-mode subtask with the next instructions you receive.` @@ -31,6 +39,8 @@ export const SUBTASK_XPROFILE_SAME_CHILD_RESULT = "Same-profile child completed" export const SUBTASK_XPROFILE_DIFFERENT_CHILD_RESULT = "Different-profile child completed" export const SUBTASK_XPROFILE_PARENT_RESULT = "Sequential cross-profile parent resumed" +const apiHangChildMatch = new RegExp(SUBTASK_API_HANG_CHILD_MARKER) + const requestContains = (req: ChatCompletionRequest, expected: string[]) => { const rawRequest = JSON.stringify(req) return expected.every((text) => rawRequest.includes(text)) @@ -167,6 +177,79 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + mock.addFixture({ + match: { + userMessage: new RegExp(SUBTASK_API_HANG_PARENT_MARKER), + sequenceIndex: 0, + }, + response: { + toolCalls: [ + { + name: "new_task", + arguments: JSON.stringify({ + mode: "ask", + message: SUBTASK_API_HANG_CHILD_PROMPT, + }), + id: "call_api_hang_parent_new_task_001", + }, + ], + }, + }) + + mock.addFixture({ + match: { + userMessage: apiHangChildMatch, + sequenceIndex: 0, + }, + // Keep the first child response pending long enough for the e2e test to cancel an in-flight API request. + latency: 15_000, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_API_HANG_CHILD_RESULT }), + id: "call_api_hang_child_completion_002", + }, + ], + }, + }) + + mock.addFixture({ + match: { + userMessage: apiHangChildMatch, + sequenceIndex: 1, + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_API_HANG_CHILD_RESULT }), + id: "call_api_hang_child_completion_003", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + requestContains(req, [ + SUBTASK_API_HANG_PARENT_MARKER, + "call_api_hang_parent_new_task_001", + SUBTASK_API_HANG_CHILD_RESULT, + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_API_HANG_PARENT_RESULT }), + id: "call_api_hang_parent_completion_004", + }, + ], + }, + }) + // Issue #457 sequence: a same-profile child returns first, then the resumed // parent delegates to a child whose mode uses a different API profile. mock.addFixture({ diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 05b8178b1f..3a81b8b492 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -5,6 +5,12 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { setDefaultSuiteTimeout } from "./test-utils" import { sleep, waitFor, waitUntilCompleted } from "./utils" import { + SUBTASK_API_HANG_CHILD_RESULT, + SUBTASK_API_HANG_CHILD_MARKER, + SUBTASK_API_HANG_PARENT_MARKER, + SUBTASK_API_HANG_PARENT_PROMPT, + SUBTASK_API_HANG_PARENT_RESULT, + SUBTASK_API_HANG_RESUME_MESSAGE, SUBTASK_CHILD_FOLLOWUP_ANSWER, SUBTASK_FAST_PARENT_PROMPT, SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER, @@ -17,6 +23,45 @@ import { SUBTASK_XPROFILE_SAME_CHILD_RESULT, } from "../fixtures/subtasks" +type AimockMessageContent = string | Array<{ type?: string; text?: string }> + +type AimockJournalEntry = { + body?: { + messages?: Array<{ + role?: string + content?: AimockMessageContent + }> + } +} + +const messageContentText = (content?: AimockMessageContent) => { + if (typeof content === "string") { + return content + } + + return content?.map((part) => part.text ?? "").join("") ?? "" +} + +const waitForAimockRequestContaining = async (expectedText: string, excludeText?: string) => { + const aimockUrl = process.env.AIMOCK_URL + assert.ok(aimockUrl, "AIMOCK_URL must be set for aimock journal assertions") + + await waitFor(async () => { + const response = await fetch(`${aimockUrl}/__aimock/journal`) + const entries = (await response.json()) as AimockJournalEntry[] + + return entries.some((entry) => { + const messages = entry.body?.messages + if (!messages) return false + const entryText = messages.map((m) => messageContentText(m.content)).join("") + if (excludeText && entryText.includes(excludeText)) return false + return messages.some( + (message) => message.role === "user" && messageContentText(message.content).includes(expectedText), + ) + }) + }) +} + suite("Roo Code Subtasks", function () { setDefaultSuiteTimeout(this) @@ -412,6 +457,113 @@ suite("Roo Code Subtasks", function () { } }) + // Issue #566: a child interrupted while its provider request is still pending + // must keep its parent link when manually resumed and completed. + test("API-hung interrupted child resumes and returns to parent", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + api.on(RooCodeEventName.Message, messageHandler) + + try { + const parentTaskId = await api.startNewTask({ + configuration: { + mode: "ask", + alwaysAllowModeSwitch: true, + alwaysAllowSubtasks: true, + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SUBTASK_API_HANG_PARENT_PROMPT, + }) + + let childTaskId: string | undefined + await waitFor(() => { + const stack = api.getCurrentTaskStack() + const current = stack[stack.length - 1] + if (current && current !== parentTaskId) { + childTaskId = current + return true + } + return false + }) + + await waitForAimockRequestContaining(SUBTASK_API_HANG_CHILD_MARKER, SUBTASK_API_HANG_PARENT_MARKER) + + await api.cancelCurrentTask() + + await waitFor(() => api.getCurrentTaskStack().at(-1) === childTaskId) + await waitFor( + () => asks[childTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + ) + + const interruptedChild = await api.getTaskHistoryItem(childTaskId!) + assert.strictEqual(interruptedChild?.status, "interrupted", "Child should be interrupted after manual stop") + assert.strictEqual( + interruptedChild?.parentTaskId, + parentTaskId, + "Interrupted child should retain its parent link before resume", + ) + + const completedParentTaskId = await waitUntilCompleted({ + api, + start: async () => { + await api.sendMessage(SUBTASK_API_HANG_RESUME_MESSAGE) + return parentTaskId + }, + }) + + assert.strictEqual( + completedParentTaskId, + parentTaskId, + "Parent task should complete after API-hung child resumes and reports back", + ) + assert.strictEqual( + says[childTaskId!] + ?.filter(({ say }) => say === "completion_result") + .map(({ text }) => text?.trim()) + .find((text) => text === SUBTASK_API_HANG_CHILD_RESULT), + SUBTASK_API_HANG_CHILD_RESULT, + "Child should complete with its expected result after resume", + ) + assert.strictEqual( + says[parentTaskId] + ?.filter(({ say }) => say === "completion_result") + .map(({ text }) => text?.trim()) + .find((text) => text === SUBTASK_API_HANG_PARENT_RESULT), + SUBTASK_API_HANG_PARENT_RESULT, + "Parent should resume and complete with its expected result", + ) + + const parent = await api.getTaskHistoryItem(parentTaskId) + assert.notStrictEqual(parent?.status, "delegated", "Parent history should not remain delegated") + assert.strictEqual(parent?.awaitingChildId, undefined, "Parent awaitingChildId should be cleared") + assert.strictEqual(parent?.completedByChildId, childTaskId, "Parent should record completed child") + + const child = await api.getTaskHistoryItem(childTaskId!) + assert.strictEqual(child?.status, "completed", "Child history should be completed") + assert.strictEqual(child?.parentTaskId, parentTaskId, "Completed child should still point to parent") + } finally { + api.off(RooCodeEventName.Message, messageHandler) + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) + } + }) + test("same-profile child returns before a different-profile child", async () => { const api = globalThis.api const says: Record = {} diff --git a/src/__tests__/history-resume-delegation.spec.ts b/src/__tests__/history-resume-delegation.spec.ts index 0be2d28a3f..13560a7434 100644 --- a/src/__tests__/history-resume-delegation.spec.ts +++ b/src/__tests__/history-resume-delegation.spec.ts @@ -26,6 +26,15 @@ vi.mock("vscode", () => { return { window, workspace, env, Uri, commands, ExtensionMode, version } }) +// Mock TelemetryService (needed by attemptCompletionTool's emitTaskCompleted) +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureTaskCompleted: vi.fn(), + }, + }, +})) + // Mock persistence BEFORE importing provider vi.mock("../core/task-persistence/taskMessages", () => ({ readTaskMessages: vi.fn().mockResolvedValue([]), @@ -1135,4 +1144,140 @@ describe("History resume delegation - parent metadata transitions", () => { expect(capturedParentResult?.completedByChildId).toBe("c-handoff") }) }) + + describe("Issue #566 — manual stop/resume of a delegated subtask", () => { + it("reopens the parent after a subtask is cancelled mid-stream, resumed, and completes (interrupted → attempt_completion)", async () => { + // Step 1: simulate cancelTask()'s persisted transition — child "active" → "interrupted", + // parent stays "delegated" with awaitingChildId intact (ClineProvider.ts cancelTaskInternal). + const childItem: Record = { + id: "child-566", + status: "interrupted", + parentTaskId: "parent-566", + ts: 1, + task: "Child task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + mode: "code", + workspace: "/tmp", + } + const parentItem: Record = { + id: "parent-566", + status: "delegated", + delegatedToId: "child-566", + awaitingChildId: "child-566", + childIds: ["child-566"], + ts: 0, + task: "Parent task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + mode: "code", + workspace: "/tmp", + } + + // Step 2: user clicks "Resume" — the extension rehydrates the child from its persisted + // history item (createTaskWithHistoryItem passes historyItem.status through as + // initialStatus), so the resumed task instance still reports "interrupted". + let currentActiveId: string | undefined = "child-566" + const emitSpy = vi.fn() + const removeClineFromStack = vi.fn().mockImplementation(async () => { + currentActiveId = undefined + }) + const createTaskWithHistoryItem = vi.fn().mockImplementation(async (historyItem: any) => { + currentActiveId = historyItem.id + return { + taskId: historyItem.id, + resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), + overwriteClineMessages: vi.fn().mockResolvedValue(undefined), + overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), + } + }) + const getTaskWithId = vi.fn(async (id: string) => { + const item = id === "child-566" ? childItem : id === "parent-566" ? parentItem : undefined + if (!item) throw new Error("Task not found") + return { historyItem: item } + }) + const taskHistoryStore = { + atomicUpdatePair: vi.fn( + async ( + firstId: string, + secondId: string, + firstUpdater: (h: any) => any, + secondUpdater: (h: any) => any, + ) => { + Object.assign(childItem, firstUpdater(childItem)) + Object.assign(parentItem, secondUpdater(parentItem)) + return [] + }, + ), + get: vi.fn((id: string) => + id === "child-566" ? childItem : id === "parent-566" ? parentItem : undefined, + ), + } + + const provider = makeProviderStub({ + contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, + getTaskWithId, + emit: emitSpy, + getCurrentTask: vi.fn(() => (currentActiveId ? ({ taskId: currentActiveId } as any) : undefined)), + removeClineFromStack, + createTaskWithHistoryItem, + taskHistoryStore, + reopenParentFromDelegation: vi.fn(async (params: any) => { + // Intentional self-reference: the provider variable is initialized before this stub is invoked. + return await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, params) + }), + } as unknown as ClineProvider) + + vi.mocked(readTaskMessages).mockResolvedValue([]) + vi.mocked(readApiMessages).mockResolvedValue([]) + + // Step 3: the resumed subtask finishes its work and calls attempt_completion. + const { attemptCompletionTool } = await import("../core/tools/AttemptCompletionTool") + const resumedChildTask = { + taskId: "child-566", + parentTask: undefined, // live parent reference is gone after resume; only parentTaskId survives + parentTaskId: "parent-566", + historyItem: { parentTaskId: "parent-566" }, + providerRef: { deref: () => provider }, + say: vi.fn().mockResolvedValue(undefined), + emit: vi.fn(), + getTokenUsage: vi.fn(() => ({})), + toolUsage: {}, + clineMessages: [], + userMessageContent: [], + consecutiveMistakeCount: 0, + emitFinalTokenUsageUpdate: vi.fn(), + } as unknown as import("../core/task/Task").Task + + const block = { + type: "tool_use", + name: "attempt_completion", + params: { result: "Child finished after resume" }, + nativeArgs: { result: "Child finished after resume" }, + partial: false, + } as any + + await attemptCompletionTool.handle(resumedChildTask, block, { + askApproval: vi.fn(), + handleError: vi.fn(async (_action: string, err: Error) => { + throw err + }), + pushToolResult: vi.fn(), + askFinishSubTaskApproval: vi.fn(async () => true), + toolDescription: () => "desc", + } as any) + + // The parent must regain control — this is the exact behavior issue #566 reported as broken. + expect(currentActiveId).toBe("parent-566") + expect(childItem.status).toBe("completed") + expect(parentItem.status).toBe("active") + expect(parentItem.awaitingChildId).toBeUndefined() + + const eventNames = emitSpy.mock.calls.map((c: any[]) => c[0]) + expect(eventNames).toContain(RooCodeEventName.TaskDelegationCompleted) + expect(eventNames).toContain(RooCodeEventName.TaskDelegationResumed) + }) + }) }) From 45968c0423f5914354cacf71d9ad1c5108734eb4 Mon Sep 17 00:00:00 2001 From: James Mtendamema <59908268+JamesRobert20@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:13:12 +0300 Subject: [PATCH 37/89] fix(webview): dismiss welcome after Zoo Gateway sign-in (#961) (#962) checkExistKey ignored Zoo Gateway session auth, so Finish saved config but never left the setup screen. Co-authored-by: Cursor --- .../__tests__/checkExistApiConfig.spec.ts | 32 +++++++++++++++++++ src/shared/checkExistApiConfig.ts | 16 +++++++++- .../src/context/ExtensionStateContext.tsx | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/shared/__tests__/checkExistApiConfig.spec.ts b/src/shared/__tests__/checkExistApiConfig.spec.ts index 4ffb394eb2..467fbcc45d 100644 --- a/src/shared/__tests__/checkExistApiConfig.spec.ts +++ b/src/shared/__tests__/checkExistApiConfig.spec.ts @@ -86,4 +86,36 @@ describe("checkExistKey", () => { } expect(checkExistKey(config)).toBe(false) }) + + it("should return false for zoo-gateway without session token or auth", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "alibaba/qwen-3.6-max-preview", + } + expect(checkExistKey(config)).toBe(false) + expect(checkExistKey(config, false)).toBe(false) + }) + + it("should return true for zoo-gateway when profile has zooSessionToken", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooSessionToken: "zoo_ext_test_token", + } + expect(checkExistKey(config)).toBe(true) + }) + + it("should return true for zoo-gateway when Zoo Code session auth is active", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "alibaba/qwen-3.6-max-preview", + } + expect(checkExistKey(config, true)).toBe(true) + }) + + it("should ignore zooCodeIsAuthenticated for non-zoo-gateway providers", () => { + const config: ProviderSettings = { + apiProvider: "openrouter", + } + expect(checkExistKey(config, true)).toBe(false) + }) }) diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 357144c168..7e5ff2f258 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -1,6 +1,14 @@ import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, ProviderSettings } from "@roo-code/types" -export function checkExistKey(config: ProviderSettings | undefined) { +/** + * Returns whether a provider profile is sufficiently configured to leave the + * welcome/setup gate. + * + * `zooCodeIsAuthenticated` is needed for Zoo Gateway: auth lives in global + * secret storage (`zoo-code-auth`), and `zooSessionToken` is not part of + * `SECRET_STATE_KEYS`, so session-auth alone would otherwise look unconfigured. + */ +export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAuthenticated?: boolean) { if (!config) { return false } @@ -10,6 +18,12 @@ export function checkExistKey(config: ProviderSettings | undefined) { return true } + // Zoo Gateway uses session auth (profile token and/or global Zoo Code login), + // not a traditional API key listed in SECRET_STATE_KEYS. + if (config.apiProvider === "zoo-gateway") { + return Boolean(config.zooSessionToken) || Boolean(zooCodeIsAuthenticated) + } + // Check all secret keys from the centralized SECRET_STATE_KEYS array. // Filter out keys that are not part of ProviderSettings (global secrets are stored separately) const providerSecretKeys = SECRET_STATE_KEYS.filter((key) => !GLOBAL_SECRET_KEYS.includes(key as any)) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 29b958319c..d0d4e37afa 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -317,7 +317,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode case "state": { const newState = message.state ?? {} setState((prevState) => mergeExtensionState(prevState, newState)) - setShowWelcome(!checkExistKey(newState.apiConfiguration)) + setShowWelcome(!checkExistKey(newState.apiConfiguration, newState.zooCodeIsAuthenticated)) setDidHydrateState(true) // Update alwaysAllowFollowupQuestions if present in state message if ((newState as any).alwaysAllowFollowupQuestions !== undefined) { From 1b372ce64781b7e910f64530ee021d11932eb942 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:15:42 +0100 Subject: [PATCH 38/89] feat: add MiniMax-M3 model (#946) Co-authored-by: Roomote --- packages/types/src/providers/minimax.ts | 17 ++++++++++- src/api/providers/__tests__/minimax.spec.ts | 33 +++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/types/src/providers/minimax.ts b/packages/types/src/providers/minimax.ts index 2ac02cf9af..7fe22e4041 100644 --- a/packages/types/src/providers/minimax.ts +++ b/packages/types/src/providers/minimax.ts @@ -5,9 +5,24 @@ import type { ModelInfo } from "../model.js" // https://platform.minimax.io/docs/guides/pricing-paygo // https://platform.minimax.io/docs/guides/pricing-tokenplan export type MinimaxModelId = keyof typeof minimaxModels -export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2.7" +export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M3" export const minimaxModels = { + "MiniMax-M3": { + maxTokens: 16_384, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], + preserveReasoning: true, + inputPrice: 0.3, + outputPrice: 1.2, + cacheWritesPrice: 0.375, + cacheReadsPrice: 0.06, + description: + "MiniMax M3, MiniMax's frontier coding model with native multimodal (image and video) input and a 1M token context window, built for agent reasoning, tool calling, and long-context tasks. Prices reflect the permanent 50% discount; input, output, and cache read prices double for requests with over 512k input tokens. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.", + }, "MiniMax-M2.5": { maxTokens: 16_384, contextWindow: 204_800, diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index d87ae1190b..5f90d2b818 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -89,6 +89,23 @@ describe("MiniMaxHandler", () => { expect(model.info).toEqual(minimaxModels[testModelId]) }) + it("should return MiniMax-M3 model with correct configuration", () => { + const testModelId: MinimaxModelId = "MiniMax-M3" + const handlerWithModel = new MiniMaxHandler({ + apiModelId: testModelId, + minimaxApiKey: "test-minimax-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(minimaxModels[testModelId]) + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.maxTokens).toBe(16_384) + expect(model.info.supportsImages).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.cacheWritesPrice).toBe(0.375) + expect(model.info.cacheReadsPrice).toBe(0.06) + }) + it("should return MiniMax-M2.5 model with correct configuration", () => { const testModelId: MinimaxModelId = "MiniMax-M2.5" const handlerWithModel = new MiniMaxHandler({ @@ -193,10 +210,10 @@ describe("MiniMaxHandler", () => { expect(model.info).toEqual(minimaxModels[minimaxDefaultModelId]) }) - it("should default to MiniMax-M2.7 model", () => { + it("should default to MiniMax-M3 model", () => { const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" }) const model = handlerDefault.getModel() - expect(model.id).toBe("MiniMax-M2.7") + expect(model.id).toBe("MiniMax-M3") }) }) @@ -398,6 +415,18 @@ describe("MiniMaxHandler", () => { }) describe("Model Configuration", () => { + it("should correctly configure MiniMax-M3 model properties", () => { + const model = minimaxModels["MiniMax-M3"] + expect(model.maxTokens).toBe(16_384) + expect(model.contextWindow).toBe(1_000_000) + expect(model.supportsImages).toBe(true) + expect(model.supportsPromptCache).toBe(true) + expect(model.inputPrice).toBe(0.3) + expect(model.outputPrice).toBe(1.2) + expect(model.cacheWritesPrice).toBe(0.375) + expect(model.cacheReadsPrice).toBe(0.06) + }) + it("should correctly configure MiniMax-M2 model properties", () => { const model = minimaxModels["MiniMax-M2"] expect(model.maxTokens).toBe(16_384) From b132a6b35919acfd17c35b52527f260a2ec62131 Mon Sep 17 00:00:00 2001 From: Konstantin Krastev Date: Tue, 21 Jul 2026 03:18:13 +0300 Subject: [PATCH 39/89] feat: enable image support for DeepSeek V4 models (#963) DeepSeek V4 Pro and V4 Flash support image inputs via their OpenAI-compatible API, but the model definitions had supportsImages: false. This change flips it to true for both models, enabling users to attach images when using these models. --- packages/types/src/providers/deepseek.ts | 4 ++-- src/api/providers/__tests__/deepseek.spec.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index 143ce12fcc..0e40c2be13 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -12,7 +12,7 @@ export const deepSeekModels = { "deepseek-v4-flash": { maxTokens: 384_000, contextWindow: 1_000_000, - supportsImages: false, + supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["disable", "low", "medium", "high", "xhigh"], preserveReasoning: true, @@ -26,7 +26,7 @@ export const deepSeekModels = { "deepseek-v4-pro": { maxTokens: 384_000, contextWindow: 1_000_000, - supportsImages: false, + supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["disable", "low", "medium", "high", "xhigh"], preserveReasoning: true, diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 563b49bce8..cc80e8769c 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -225,6 +225,7 @@ describe("DeepSeekHandler", () => { expect(model.id).toBe("deepseek-v4-flash") expect(model.info.maxTokens).toBe(384_000) expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsImages).toBe(true) expect((model.info as ModelInfo).supportsReasoningEffort).toContain("xhigh") }) @@ -252,6 +253,7 @@ describe("DeepSeekHandler", () => { expect(model.info).toBeDefined() expect(model.info.maxTokens).toBe(384_000) expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsImages).toBe(true) expect(model.info.supportsPromptCache).toBe(true) expect((model.info as ModelInfo).preserveReasoning).toBe(true) expect((model.info as ModelInfo).reasoningEffort).toBe("high") From f2bdcb6da6235dbac9b4aa47b0cb73a5cacfae1b Mon Sep 17 00:00:00 2001 From: Konstantin Krastev Date: Tue, 21 Jul 2026 16:07:01 +0300 Subject: [PATCH 40/89] feat(moonshot): implement provider with model picker, API fetching, streaming, and model metadata (#857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(moonshot): fix provider issues with model picker, refresh, and streaming - Fix duplicate ModelPicker by adding moonshot to PROVIDERS_WITH_CUSTOM_MODEL_UI - Fix HTTP 404 on Refresh Models by preserving /v1 in fetcher URL - Fix empty model list (consequence of 404 fix) - Fix 'No output generated' error by switching MoonshotHandler from OpenAICompatibleHandler (Vercel AI SDK) to OpenAiHandler (OpenAI Node SDK) which is what Moonshot's own API docs recommend - Add dedicated Moonshot fetcher with proper model ID prefixing - Update tests to match new handler implementation All 6,660 backend and 1,432 webview-ui tests pass. * feat(moonshot): add missing models with correct pricing from official docs Add kimi-k2.7-code, kimi-k2.7-code-highspeed, and kimi-k2.6 to static models map with accurate pricing from Moonshot documentation. Also fix kimi-k2.5 to correctly mark supportsImages: true. * Update webview-ui/src/components/settings/providers/Moonshot.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(moonshot): remove fallback pricing for unknown models to match handler behavior * Bump coverage * fix(moonshot): address PR review feedback — add tests and fix inline import type - Replace inline import() type with top-level OpenAI.Chat.Completions.X in moonshot.ts - Add useSelectedModel moonshot tests (default model, router override, unknown fallback, getValidatedModelId) - Add modelCache moonshot dispatch test for fetchModelsFromProvider switch - Create Moonshot.spec.tsx with state machine tests (idle/loading/success/error states, race condition guard) --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/types/src/provider-settings.ts | 1 + packages/types/src/providers/moonshot.ts | 40 +- src/api/providers/__tests__/moonshot.spec.ts | 355 ++++++++-------- .../fetchers/__tests__/modelCache.spec.ts | 24 ++ .../fetchers/__tests__/moonshot.spec.ts | 180 ++++++++ src/api/providers/fetchers/modelCache.ts | 6 + src/api/providers/fetchers/moonshot.ts | 89 ++++ src/api/providers/moonshot.ts | 82 ++-- .../webview/__tests__/ClineProvider.spec.ts | 3 + ...webviewMessageHandler.routerModels.spec.ts | 182 ++++++++ .../__tests__/webviewMessageHandler.spec.ts | 3 + src/core/webview/webviewMessageHandler.ts | 16 + src/shared/api.ts | 1 + .../settings/providers/Moonshot.tsx | 108 ++++- .../providers/__tests__/Moonshot.spec.tsx | 397 ++++++++++++++++++ .../settings/utils/providerModelConfig.ts | 1 + .../hooks/__tests__/useSelectedModel.spec.ts | 97 +++++ .../components/ui/hooks/useSelectedModel.ts | 10 +- .../src/utils/__tests__/validate.spec.ts | 1 + 19 files changed, 1365 insertions(+), 231 deletions(-) create mode 100644 src/api/providers/fetchers/__tests__/moonshot.spec.ts create mode 100644 src/api/providers/fetchers/moonshot.ts create mode 100644 webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index fb03531c33..721b413416 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -46,6 +46,7 @@ export const dynamicProviders = [ "unbound", "poe", "deepseek", + "moonshot", "opencode-go", "kenari", ] as const diff --git a/packages/types/src/providers/moonshot.ts b/packages/types/src/providers/moonshot.ts index a825475644..ad729bc2a7 100644 --- a/packages/types/src/providers/moonshot.ts +++ b/packages/types/src/providers/moonshot.ts @@ -56,7 +56,7 @@ export const moonshotModels = { "kimi-k2.5": { maxTokens: 16_384, contextWindow: 262_144, - supportsImages: false, + supportsImages: true, // Supports text, image, and video input supportsPromptCache: true, inputPrice: 0.6, // $0.60 per million tokens (cache miss) outputPrice: 3.0, // $3.00 per million tokens @@ -64,7 +64,43 @@ export const moonshotModels = { supportsTemperature: true, defaultTemperature: 1.0, description: - "Kimi K2.5 is the latest generation of Moonshot AI's Kimi series, featuring improved reasoning capabilities and enhanced performance across diverse tasks.", + "Kimi K2.5 supports text, image, and video input, thinking and non-thinking modes, and dialogue and agent tasks. Context length 256k.", + }, + "kimi-k2.6": { + maxTokens: 16_384, + contextWindow: 262_144, + supportsImages: true, // Native multimodal: text, image, video + supportsPromptCache: true, + inputPrice: 0.95, // $0.95 per million tokens (cache miss) + outputPrice: 4.0, // $4.00 per million tokens + cacheWritesPrice: 0, // $0 per million tokens (cache writes) + cacheReadsPrice: 0.16, // $0.16 per million tokens (cache hit) + description: + "Kimi K2.6 is Kimi's latest and most intelligent model with stronger long-term code writing capabilities, improved instruction compliance, and self-correction. Native multimodal architecture supporting text, image, and video input. Context length 256k.", + }, + "kimi-k2.7-code": { + maxTokens: 16_384, + contextWindow: 262_144, + supportsImages: true, // Native multimodal: text, image, video + supportsPromptCache: true, + inputPrice: 0.95, // $0.95 per million tokens (cache miss) + outputPrice: 4.0, // $4.00 per million tokens + cacheWritesPrice: 0, // $0 per million tokens (cache writes) + cacheReadsPrice: 0.19, // $0.19 per million tokens (cache hit) + description: + "Kimi K2.7 Code is Kimi's most intelligent Coding model for higher success rates in long context programming tasks. Native multimodal architecture supporting text, image, and video input. Context length 256k.", + }, + "kimi-k2.7-code-highspeed": { + maxTokens: 16_384, + contextWindow: 262_144, + supportsImages: true, // Native multimodal: text, image, video + supportsPromptCache: true, + inputPrice: 1.9, // $1.90 per million tokens (cache miss) + outputPrice: 8.0, // $8.00 per million tokens + cacheWritesPrice: 0, // $0 per million tokens (cache writes) + cacheReadsPrice: 0.38, // $0.38 per million tokens (cache hit) + description: + "Kimi K2.7 Code HighSpeed is the high-speed version of Kimi K2.7 Code with output speed of approximately 180 Tokens/s (up to 260 Tokens/s in short context). Same model architecture, faster output. Context length 256k.", }, } as const satisfies Record diff --git a/src/api/providers/__tests__/moonshot.spec.ts b/src/api/providers/__tests__/moonshot.spec.ts index c0fd832a19..ab8f818697 100644 --- a/src/api/providers/__tests__/moonshot.spec.ts +++ b/src/api/providers/__tests__/moonshot.spec.ts @@ -1,28 +1,3 @@ -// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls -const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({ - mockStreamText: vi.fn(), - mockGenerateText: vi.fn(), -})) - -vi.mock("ai", async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - streamText: mockStreamText, - generateText: mockGenerateText, - } -}) - -vi.mock("@ai-sdk/openai-compatible", () => ({ - createOpenAICompatible: vi.fn(function () { - // Return a function that returns a mock language model - return vi.fn(() => ({ - modelId: "moonshot-chat", - provider: "moonshot", - })) - }), -})) - import type { Anthropic } from "@anthropic-ai/sdk" import { moonshotDefaultModelId } from "@roo-code/types" @@ -38,7 +13,7 @@ describe("MoonshotHandler", () => { beforeEach(() => { mockOptions = { moonshotApiKey: "test-api-key", - apiModelId: "moonshot-chat", + apiModelId: "kimi-k2-0905-preview", moonshotBaseUrl: "https://api.moonshot.ai/v1", } handler = new MoonshotHandler(mockOptions) @@ -96,9 +71,16 @@ describe("MoonshotHandler", () => { const model = handlerWithInvalidModel.getModel() expect(model.id).toBe("invalid-model") // Returns provided ID expect(model.info).toBeDefined() - // Should have the same base properties as default model + // Should have the same structural properties as default model expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow) expect(model.info.supportsPromptCache).toBe(true) + // Unknown models should not send a guessed maxTokens to the API + expect(model.info.maxTokens).toBeUndefined() + // Pricing should be unknown for unrecognized models + expect(model.info.inputPrice).toBeUndefined() + expect(model.info.outputPrice).toBeUndefined() + expect(model.info.cacheReadsPrice).toBeUndefined() + expect(model.info.cacheWritesPrice).toBeUndefined() }) it("should return default model if no model ID is provided", () => { @@ -134,23 +116,22 @@ describe("MoonshotHandler", () => { ] it("should handle streaming responses", async () => { - // Mock the fullStream async generator - async function* mockFullStream() { - yield { type: "text-delta", text: "Test response" } + async function* mockStream() { + yield { + choices: [{ delta: { content: "Test response" }, finish_reason: null }], + usage: null, + } } - // Mock usage promise - const mockUsage = Promise.resolve({ - inputTokens: 10, - outputTokens: 5, - details: { cachedInputTokens: undefined }, - raw: { cached_tokens: 2 }, - }) + const mockClient = { + chat: { + completions: { + create: vi.fn().mockResolvedValue(mockStream()), + }, + }, + } - mockStreamText.mockReturnValue({ - fullStream: mockFullStream(), - usage: mockUsage, - }) + ;(handler as any).client = mockClient const stream = handler.createMessage(systemPrompt, messages) const chunks: any[] = [] @@ -158,28 +139,28 @@ describe("MoonshotHandler", () => { chunks.push(chunk) } - expect(chunks.length).toBeGreaterThan(0) const textChunks = chunks.filter((chunk) => chunk.type === "text") expect(textChunks).toHaveLength(1) expect(textChunks[0].text).toBe("Test response") }) it("should include usage information", async () => { - async function* mockFullStream() { - yield { type: "text-delta", text: "Test response" } + async function* mockStream() { + yield { + choices: [{ delta: { content: "Test response" }, finish_reason: "stop" }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } } - const mockUsage = Promise.resolve({ - inputTokens: 10, - outputTokens: 5, - details: {}, - raw: { cached_tokens: 2 }, - }) + const mockClient = { + chat: { + completions: { + create: vi.fn().mockResolvedValue(mockStream()), + }, + }, + } - mockStreamText.mockReturnValue({ - fullStream: mockFullStream(), - usage: mockUsage, - }) + ;(handler as any).client = mockClient const stream = handler.createMessage(systemPrompt, messages) const chunks: any[] = [] @@ -194,21 +175,26 @@ describe("MoonshotHandler", () => { }) it("should include cache metrics in usage information", async () => { - async function* mockFullStream() { - yield { type: "text-delta", text: "Test response" } + async function* mockStream() { + yield { + choices: [{ delta: { content: "Test response" }, finish_reason: "stop" }], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + prompt_tokens_details: { cached_tokens: 2 }, + }, + } } - const mockUsage = Promise.resolve({ - inputTokens: 10, - outputTokens: 5, - details: {}, - raw: { cached_tokens: 2 }, - }) + const mockClient = { + chat: { + completions: { + create: vi.fn().mockResolvedValue(mockStream()), + }, + }, + } - mockStreamText.mockReturnValue({ - fullStream: mockFullStream(), - usage: mockUsage, - }) + ;(handler as any).client = mockClient const stream = handler.createMessage(systemPrompt, messages) const chunks: any[] = [] @@ -224,25 +210,34 @@ describe("MoonshotHandler", () => { }) describe("completePrompt", () => { - it("should complete a prompt using generateText", async () => { - mockGenerateText.mockResolvedValue({ - text: "Test completion", - }) + it("should complete a prompt using the OpenAI client", async () => { + const mockClient = { + chat: { + completions: { + create: vi.fn().mockResolvedValue({ + choices: [{ message: { content: "Test completion" } }], + }), + }, + }, + } + + ;(handler as any).client = mockClient const result = await handler.completePrompt("Test prompt") expect(result).toBe("Test completion") - expect(mockGenerateText).toHaveBeenCalledWith( + expect(mockClient.chat.completions.create).toHaveBeenCalledWith( expect.objectContaining({ - prompt: "Test prompt", + model: mockOptions.apiModelId, + messages: [{ role: "user", content: "Test prompt" }], }), + {}, ) }) }) describe("processUsageMetrics", () => { it("should correctly process usage metrics including cache information", () => { - // We need to access the protected method, so we'll create a test subclass class TestMoonshotHandler extends MoonshotHandler { public testProcessUsageMetrics(usage: any) { return this.processUsageMetrics(usage) @@ -252,10 +247,9 @@ describe("MoonshotHandler", () => { const testHandler = new TestMoonshotHandler(mockOptions) const usage = { - inputTokens: 100, - outputTokens: 50, - details: {}, - raw: { + prompt_tokens: 100, + completion_tokens: 50, + prompt_tokens_details: { cached_tokens: 20, }, } @@ -279,10 +273,8 @@ describe("MoonshotHandler", () => { const testHandler = new TestMoonshotHandler(mockOptions) const usage = { - inputTokens: 100, - outputTokens: 50, - details: {}, - raw: {}, + prompt_tokens: 100, + completion_tokens: 50, } const result = testHandler.testProcessUsageMetrics(usage) @@ -293,27 +285,64 @@ describe("MoonshotHandler", () => { expect(result.cacheWriteTokens).toBe(0) expect(result.cacheReadTokens).toBeUndefined() }) + + it("should handle cached_tokens at top level (not in prompt_tokens_details)", () => { + class TestMoonshotHandler extends MoonshotHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) + } + } + + const testHandler = new TestMoonshotHandler(mockOptions) + + const usage = { + prompt_tokens: 100, + completion_tokens: 50, + cached_tokens: 15, + } + + const result = testHandler.testProcessUsageMetrics(usage) + + expect(result.cacheReadTokens).toBe(15) + }) + + it("should handle null usage gracefully", () => { + class TestMoonshotHandler extends MoonshotHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) + } + } + + const testHandler = new TestMoonshotHandler(mockOptions) + + const result = testHandler.testProcessUsageMetrics(null) + + expect(result.inputTokens).toBe(0) + expect(result.outputTokens).toBe(0) + expect(result.cacheReadTokens).toBeUndefined() + }) }) - describe("getMaxOutputTokens", () => { - it("should return maxTokens from model info", () => { + describe("addMaxTokensIfNeeded", () => { + it("should use max_tokens (not max_completion_tokens) for Moonshot", () => { class TestMoonshotHandler extends MoonshotHandler { - public testGetMaxOutputTokens() { - return this.getMaxOutputTokens() + public testAddMaxTokensIfNeeded(requestOptions: any, modelInfo: any) { + return this.addMaxTokensIfNeeded(requestOptions, modelInfo) } } const testHandler = new TestMoonshotHandler(mockOptions) - const result = testHandler.testGetMaxOutputTokens() + const requestOptions: any = {} + testHandler.testAddMaxTokensIfNeeded(requestOptions, handler.getModel().info) - // Default model maxTokens is 16384 - expect(result).toBe(16384) + expect(requestOptions.max_tokens).toBe(16384) + expect(requestOptions.max_completion_tokens).toBeUndefined() }) - it("should use modelMaxTokens when provided", () => { + it("should use modelMaxTokens override when provided", () => { class TestMoonshotHandler extends MoonshotHandler { - public testGetMaxOutputTokens() { - return this.getMaxOutputTokens() + public testAddMaxTokensIfNeeded(requestOptions: any, modelInfo: any) { + return this.addMaxTokensIfNeeded(requestOptions, modelInfo) } } @@ -322,23 +351,27 @@ describe("MoonshotHandler", () => { ...mockOptions, modelMaxTokens: customMaxTokens, }) + const requestOptions: any = {} + testHandler.testAddMaxTokensIfNeeded(requestOptions, handler.getModel().info) - const result = testHandler.testGetMaxOutputTokens() - expect(result).toBe(customMaxTokens) + expect(requestOptions.max_tokens).toBe(customMaxTokens) }) - it("should fall back to modelInfo.maxTokens when modelMaxTokens is not provided", () => { + it("should not send maxTokens for unknown model IDs", () => { class TestMoonshotHandler extends MoonshotHandler { - public testGetMaxOutputTokens() { - return this.getMaxOutputTokens() + public testAddMaxTokensIfNeeded(requestOptions: any, modelInfo: any) { + return this.addMaxTokensIfNeeded(requestOptions, modelInfo) } } - const testHandler = new TestMoonshotHandler(mockOptions) - const result = testHandler.testGetMaxOutputTokens() + const testHandler = new TestMoonshotHandler({ + ...mockOptions, + apiModelId: "future-moonshot-model", + }) + const requestOptions: any = {} + testHandler.testAddMaxTokensIfNeeded(requestOptions, testHandler.getModel().info) - // moonshot-chat has maxTokens of 16384 - expect(result).toBe(16384) + expect(requestOptions.max_tokens).toBeUndefined() }) }) @@ -352,94 +385,39 @@ describe("MoonshotHandler", () => { ] it("should handle tool calls in streaming", async () => { - async function* mockFullStream() { - yield { - type: "tool-input-start", - id: "tool-call-1", - toolName: "read_file", - } + async function* mockStream() { yield { - type: "tool-input-delta", - id: "tool-call-1", - delta: '{"path":"test.ts"}', - } - yield { - type: "tool-input-end", - id: "tool-call-1", - } - } - - const mockUsage = Promise.resolve({ - inputTokens: 10, - outputTokens: 5, - details: {}, - raw: {}, - }) - - mockStreamText.mockReturnValue({ - fullStream: mockFullStream(), - usage: mockUsage, - }) - - const stream = handler.createMessage(systemPrompt, messages, { - taskId: "test-task", - tools: [ - { - type: "function", - function: { - name: "read_file", - description: "Read a file", - parameters: { - type: "object", - properties: { path: { type: "string" } }, - required: ["path"], + choices: [ + { + delta: { + content: null, + tool_calls: [ + { + index: 0, + id: "tool-call-1", + function: { + name: "read_file", + arguments: '{"path":"test.ts"}', + }, + }, + ], }, + finish_reason: "tool_calls", }, - }, - ], - }) - - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - const toolCallStartChunks = chunks.filter((c) => c.type === "tool_call_start") - const toolCallDeltaChunks = chunks.filter((c) => c.type === "tool_call_delta") - const toolCallEndChunks = chunks.filter((c) => c.type === "tool_call_end") - - expect(toolCallStartChunks.length).toBe(1) - expect(toolCallStartChunks[0].id).toBe("tool-call-1") - expect(toolCallStartChunks[0].name).toBe("read_file") - - expect(toolCallDeltaChunks.length).toBe(1) - expect(toolCallDeltaChunks[0].delta).toBe('{"path":"test.ts"}') - - expect(toolCallEndChunks.length).toBe(1) - expect(toolCallEndChunks[0].id).toBe("tool-call-1") - }) - - it("should handle complete tool calls", async () => { - async function* mockFullStream() { - yield { - type: "tool-call", - toolCallId: "tool-call-1", - toolName: "read_file", - input: { path: "test.ts" }, + ], + usage: null, } } - const mockUsage = Promise.resolve({ - inputTokens: 10, - outputTokens: 5, - details: {}, - raw: {}, - }) + const mockClient = { + chat: { + completions: { + create: vi.fn().mockResolvedValue(mockStream()), + }, + }, + } - mockStreamText.mockReturnValue({ - fullStream: mockFullStream(), - usage: mockUsage, - }) + ;(handler as any).client = mockClient const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", @@ -464,11 +442,16 @@ describe("MoonshotHandler", () => { chunks.push(chunk) } - const toolCallChunks = chunks.filter((c) => c.type === "tool_call") - expect(toolCallChunks.length).toBe(1) - expect(toolCallChunks[0].id).toBe("tool-call-1") - expect(toolCallChunks[0].name).toBe("read_file") - expect(toolCallChunks[0].arguments).toBe('{"path":"test.ts"}') + const partialChunks = chunks.filter((c) => c.type === "tool_call_partial") + const endChunks = chunks.filter((c) => c.type === "tool_call_end") + + expect(partialChunks.length).toBe(1) + expect(partialChunks[0].id).toBe("tool-call-1") + expect(partialChunks[0].name).toBe("read_file") + expect(partialChunks[0].arguments).toBe('{"path":"test.ts"}') + + expect(endChunks.length).toBe(1) + expect(endChunks[0].id).toBe("tool-call-1") }) }) }) diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 4b851d7d1f..23586b1364 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -44,6 +44,7 @@ vi.mock("../litellm") vi.mock("../openrouter") vi.mock("../requesty") vi.mock("../kenari") +vi.mock("../moonshot") // Mock ContextProxy with a simple static instance vi.mock("../../../core/config/ContextProxy", () => ({ @@ -65,11 +66,13 @@ import { getLiteLLMModels } from "../litellm" import { getOpenRouterModels } from "../openrouter" import { getRequestyModels } from "../requesty" import { getKenariModels } from "../kenari" +import { getMoonshotModels } from "../moonshot" const mockGetLiteLLMModels = getLiteLLMModels as Mock const mockGetOpenRouterModels = getOpenRouterModels as Mock const mockGetRequestyModels = getRequestyModels as Mock const mockGetKenariModels = getKenariModels as Mock +const mockGetMoonshotModels = getMoonshotModels as Mock const DUMMY_REQUESTY_KEY = "requesty-key-for-testing" @@ -163,6 +166,27 @@ describe("getModels with new GetModelsOptions", () => { ).rejects.toThrow("LiteLLM connection failed") }) + it("calls getMoonshotModels with correct parameters", async () => { + const mockModels = { + "kimi-k2-0905-preview": { + maxTokens: 16384, + contextWindow: 262144, + supportsPromptCache: true, + description: "Moonshot Kimi K2", + }, + } + mockGetMoonshotModels.mockResolvedValue(mockModels) + + const result = await getModels({ + provider: "moonshot", + apiKey: "test-key", + baseUrl: "https://api.moonshot.ai/v1", + }) + + expect(mockGetMoonshotModels).toHaveBeenCalledWith("https://api.moonshot.ai/v1", "test-key") + expect(result).toEqual(mockModels) + }) + it("validates exhaustive provider checking with unknown provider", async () => { // This test ensures TypeScript catches unknown providers at compile time // In practice, the discriminated union should prevent this at compile time diff --git a/src/api/providers/fetchers/__tests__/moonshot.spec.ts b/src/api/providers/fetchers/__tests__/moonshot.spec.ts new file mode 100644 index 0000000000..786c6dfbdf --- /dev/null +++ b/src/api/providers/fetchers/__tests__/moonshot.spec.ts @@ -0,0 +1,180 @@ +import { moonshotModels } from "@roo-code/types" + +import { getMoonshotModels } from "../moonshot" + +describe("getMoonshotModels", () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + vi.restoreAllMocks() + }) + + it("merges API response with static model specs for known models", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + data: [{ id: "kimi-k2-0905-preview" }, { id: "kimi-k2-thinking" }], + }), + }) as unknown as typeof fetch + + const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key") + + expect(globalThis.fetch).toHaveBeenCalledWith("https://api.moonshot.ai/v1/models", expect.any(Object)) + expect(models["kimi-k2-0905-preview"]).toEqual(moonshotModels["kimi-k2-0905-preview"]) + expect(models["kimi-k2-thinking"]).toEqual(moonshotModels["kimi-k2-thinking"]) + }) + + it("provides sane defaults for unknown model IDs without pricing", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + data: [{ id: "unknown-model-id" }], + }), + }) as unknown as typeof fetch + + const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key") + + expect(models["unknown-model-id"]).toEqual({ + maxTokens: 16_000, + contextWindow: 262_144, + supportsImages: false, + supportsPromptCache: true, + description: "Moonshot model: unknown-model-id", + }) + }) + + it("throws for HTTP errors", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + statusText: "Unauthorized", + text: vi.fn().mockResolvedValue('{"error":{"message":"Invalid API key"}}'), + }) as unknown as typeof fetch + + await expect(getMoonshotModels("https://api.moonshot.ai/v1", "invalid-key")).rejects.toThrow( + "HTTP 401: Unauthorized", + ) + }) + + it("uses default base URL when none provided", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: [] }), + }) as unknown as typeof fetch + + await getMoonshotModels(undefined, "mock-key") + + expect(globalThis.fetch).toHaveBeenCalledWith("https://api.moonshot.ai/v1/models", expect.any(Object)) + }) + + it("keeps /v1 in base URL and strips trailing slash", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: [] }), + }) as unknown as typeof fetch + + await getMoonshotModels("https://api.moonshot.cn/v1/", "mock-key") + + expect(globalThis.fetch).toHaveBeenCalledWith("https://api.moonshot.cn/v1/models", expect.any(Object)) + }) + + it("throws when response data is not an array", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: "not-an-array" }), + }) as unknown as typeof fetch + + await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow( + "Unexpected response format", + ) + }) + + it("throws when response data is missing", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({}), + }) as unknown as typeof fetch + + await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow( + "Unexpected response format", + ) + }) + + it("skips models with empty or non-string ID", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + data: [{ id: "" }, { id: 123 }, { id: null }, { id: "kimi-k2-0905-preview" }], + }), + }) as unknown as typeof fetch + + const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key") + + expect(Object.keys(models)).toHaveLength(1) + expect(models["kimi-k2-0905-preview"]).toBeDefined() + }) + + it("includes Authorization header when apiKey provided", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: [] }), + }) as unknown as typeof fetch + + await getMoonshotModels("https://api.moonshot.ai/v1", "my-secret-key") + + expect(globalThis.fetch).toHaveBeenCalledWith( + "https://api.moonshot.ai/v1/models", + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer my-secret-key", + }), + }), + ) + }) + + it("does not include Authorization header when no apiKey", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: [] }), + }) as unknown as typeof fetch + + await getMoonshotModels("https://api.moonshot.ai/v1", undefined) + + const callArgs = (globalThis.fetch as any).mock.calls[0][1].headers + expect(callArgs["Authorization"]).toBeUndefined() + }) + + it("mixes known and unknown models in same response", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + data: [{ id: "kimi-k2-0905-preview" }, { id: "some-new-model" }], + }), + }) as unknown as typeof fetch + + const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key") + + expect(models["kimi-k2-0905-preview"]).toEqual(moonshotModels["kimi-k2-0905-preview"]) + expect(models["some-new-model"]).toEqual({ + maxTokens: 16_000, + contextWindow: 262_144, + supportsImages: false, + supportsPromptCache: true, + description: "Moonshot model: some-new-model", + }) + }) + + it("handles HTTP error with unreadable body gracefully", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + text: vi.fn().mockRejectedValue(new Error("network error")), + }) as unknown as typeof fetch + + await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow( + "HTTP 500: Internal Server Error", + ) + }) +}) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index deca6a7c00..c9e7784949 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -29,6 +29,7 @@ import { getOllamaModels } from "./ollama" import { getLMStudioModels } from "./lmstudio" import { getPoeModels } from "./poe" import { getDeepSeekModels } from "./deepseek" +import { getMoonshotModels } from "./moonshot" import { getZooGatewayModels } from "./zoo-gateway" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -54,6 +55,7 @@ const URL_SCOPED_PROVIDERS: ReadonlySet = new Set([ "litellm", "poe", "deepseek", + "moonshot", "ollama", "lmstudio", "requesty", @@ -66,6 +68,7 @@ const KEY_SCOPED_PROVIDERS: ReadonlySet = new Set([ "litellm", // Per-key model allowlists are a first-class LiteLLM proxy feature "poe", // Per-account model availability "requesty", // Per-account custom model policies + "moonshot", // Per-key model visibility (api.moonshot.ai vs api.moonshot.cn) ]) function isAuthScopedProvider(provider: RouterName): boolean { @@ -221,6 +224,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + // Moonshot API uses OpenAI-compatible /v1/models endpoint. + // The base URL from settings already includes /v1 (e.g. https://api.moonshot.ai/v1), + // so we keep it as-is and append /models directly. + const base = (baseUrl || "https://api.moonshot.ai/v1").replace(/\/+$/, "") + const url = `${base}/models` + + const headers: Record = { + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + } + + if (apiKey) { + headers["Authorization"] = `Bearer ${apiKey}` + } + + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 10000) + + try { + const response = await fetch(url, { + headers, + signal: controller.signal, + }) + + if (!response.ok) { + let errorBody = "" + try { + errorBody = await response.text() + } catch { + errorBody = "(unable to read response body)" + } + + console.error(`[getMoonshotModels] HTTP error:`, { + status: response.status, + statusText: response.statusText, + url, + body: errorBody, + }) + + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (!data?.data || !Array.isArray(data.data)) { + console.error("[getMoonshotModels] Unexpected response format:", data) + throw new Error("Failed to fetch Moonshot models: Unexpected response format.") + } + + // Use null-prototype object to prevent prototype pollution + const models: ModelRecord = Object.create(null) + + for (const model of data.data) { + const modelId = typeof model.id === "string" && model.id ? model.id : null + if (!modelId) continue + + const knownSpecs = moonshotModels[modelId as keyof typeof moonshotModels] + + if (knownSpecs) { + models[modelId] = { ...knownSpecs } + } else { + models[modelId] = { + maxTokens: 16_000, + contextWindow: 262_144, + supportsImages: false, + supportsPromptCache: true, + description: `Moonshot model: ${modelId}`, + } + } + } + + return models + } finally { + clearTimeout(timeoutId) + } +} diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts index 3e90e48f7a..42bd2bfaf7 100644 --- a/src/api/providers/moonshot.ts +++ b/src/api/providers/moonshot.ts @@ -1,3 +1,5 @@ +import OpenAI from "openai" + import { moonshotModels, moonshotDefaultModelId, type ModelInfo } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -5,30 +7,47 @@ import type { ApiHandlerOptions } from "../../shared/api" import type { ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" -import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible" +import { OpenAiHandler } from "./openai" -export class MoonshotHandler extends OpenAICompatibleHandler { +export class MoonshotHandler extends OpenAiHandler { constructor(options: ApiHandlerOptions) { - const modelId = options.apiModelId ?? moonshotDefaultModelId - const modelInfo = - moonshotModels[modelId as keyof typeof moonshotModels] || moonshotModels[moonshotDefaultModelId] + // Map Moonshot-specific options to the OpenAI-compatible options that + // OpenAiHandler expects. This makes Moonshot use the same battle-tested + // OpenAI Node SDK path as the generic "OpenAI Compatible" provider. + super({ + ...options, + openAiApiKey: options.moonshotApiKey ?? "not-provided", + openAiModelId: options.apiModelId ?? moonshotDefaultModelId, + openAiBaseUrl: options.moonshotBaseUrl || "https://api.moonshot.ai/v1", + }) + } - const config: OpenAICompatibleConfig = { - providerName: "moonshot", - baseURL: options.moonshotBaseUrl || "https://api.moonshot.ai/v1", - apiKey: options.moonshotApiKey ?? "not-provided", - modelId, - modelInfo, - modelMaxTokens: options.modelMaxTokens ?? undefined, - temperature: options.modelTemperature ?? undefined, + /** + * Resolve the ModelInfo for a given Moonshot model ID. + * Unknown IDs (e.g. dynamically fetched future models) keep the configured ID + * but fall back to the default model's structural metadata with pricing stripped + * so cost reporting shows "unknown" instead of charging the default model's rates. + */ + private static resolveModelInfo(modelId: string): ModelInfo { + const knownInfo = moonshotModels[modelId as keyof typeof moonshotModels] + if (knownInfo) { + return knownInfo } - super(options, config) + const defaultInfo = moonshotModels[moonshotDefaultModelId] + return { + ...defaultInfo, + maxTokens: undefined, + inputPrice: undefined, + outputPrice: undefined, + cacheReadsPrice: undefined, + cacheWritesPrice: undefined, + } } override getModel() { - const id = this.options.apiModelId ?? moonshotDefaultModelId - const info = moonshotModels[id as keyof typeof moonshotModels] || moonshotModels[moonshotDefaultModelId] + const id = this.options.openAiModelId ?? moonshotDefaultModelId + const info = MoonshotHandler.resolveModelInfo(id) const params = getModelParams({ format: "openai", modelId: id, @@ -43,24 +62,13 @@ export class MoonshotHandler extends OpenAICompatibleHandler { * Override to handle Moonshot's usage metrics, including caching. * Moonshot returns cached_tokens in a different location than standard OpenAI. */ - protected override processUsageMetrics(usage: { - inputTokens?: number - outputTokens?: number - details?: { - cachedInputTokens?: number - reasoningTokens?: number - } - raw?: Record - }): ApiStreamUsageChunk { - // Moonshot uses cached_tokens at the top level of raw usage data - const rawUsage = usage.raw as { cached_tokens?: number } | undefined - + protected override processUsageMetrics(usage: any, _modelInfo?: ModelInfo): ApiStreamUsageChunk { return { type: "usage", - inputTokens: usage.inputTokens || 0, - outputTokens: usage.outputTokens || 0, + inputTokens: usage?.prompt_tokens || 0, + outputTokens: usage?.completion_tokens || 0, cacheWriteTokens: 0, - cacheReadTokens: rawUsage?.cached_tokens ?? usage.details?.cachedInputTokens, + cacheReadTokens: usage?.prompt_tokens_details?.cached_tokens ?? usage?.cached_tokens, } } @@ -68,9 +76,13 @@ export class MoonshotHandler extends OpenAICompatibleHandler { * Override to always include max_tokens for Moonshot (not max_completion_tokens). * Moonshot requires max_tokens parameter to be sent. */ - protected override getMaxOutputTokens(): number | undefined { - const modelInfo = this.config.modelInfo - // Moonshot always requires max_tokens - return this.options.modelMaxTokens || modelInfo.maxTokens || undefined + protected override addMaxTokensIfNeeded( + requestOptions: + | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming + | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, + modelInfo: ModelInfo, + ): void { + // Moonshot always requires max_tokens (not max_completion_tokens) + requestOptions.max_tokens = this.options.modelMaxTokens || modelInfo.maxTokens || undefined } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index b1e5df93f9..7af3752b0d 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2703,6 +2703,7 @@ describe("ClineProvider - Router Models", () => { lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, @@ -2755,6 +2756,7 @@ describe("ClineProvider - Router Models", () => { litellm: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, @@ -2853,6 +2855,7 @@ describe("ClineProvider - Router Models", () => { lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts index 13af478c07..a50e73cb16 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts @@ -111,8 +111,11 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { expect(routerModels).toHaveProperty("openrouter") expect(routerModels).toHaveProperty("requesty") expect(routerModels).toHaveProperty("deepseek") + expect(routerModels).toHaveProperty("moonshot") expect(routerModels.deepseek).toEqual({}) + expect(routerModels.moonshot).toEqual({}) expect(getModelsMock).not.toHaveBeenCalledWith(expect.objectContaining({ provider: "deepseek" })) + expect(getModelsMock).not.toHaveBeenCalledWith(expect.objectContaining({ provider: "moonshot" })) }) it("fetches DeepSeek models when stored DeepSeek credentials exist", async () => { @@ -292,4 +295,183 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { baseUrl: "http://stored:4000", }) }) + + it("fetches Moonshot models when stored Moonshot credentials exist", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { + moonshotApiKey: "stored-moonshot-key", + moonshotBaseUrl: "https://api.moonshot.ai/v1", + }, + }) + + getModelsMock.mockImplementation(async (options: any) => { + if (options?.provider === "moonshot") { + return { "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true } } + } + + switch (options?.provider) { + case "openrouter": + return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } } + case "requesty": + return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } + case "vercel-ai-gateway": + return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case "litellm": + return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } + default: + return {} + } + }) + + await webviewMessageHandler( + mockProvider as any, + { + type: "requestRouterModels", + } as any, + ) + + expect(getModelsMock).toHaveBeenCalledWith({ + provider: "moonshot", + apiKey: "stored-moonshot-key", + baseUrl: "https://api.moonshot.ai/v1", + }) + + const call = (mockProvider.postMessageToWebview as any).mock.calls.find( + (c: any[]) => c[0]?.type === "routerModels", + ) + expect(call).toBeTruthy() + expect(call[0].routerModels.moonshot).toEqual({ + "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true }, + }) + }) + + it("flushes Moonshot cache when explicit apiKey provided via message values", async () => { + getModelsMock.mockResolvedValue({ + "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true }, + }) + + await webviewMessageHandler( + mockProvider as any, + { + type: "requestRouterModels", + values: { + moonshotApiKey: "new-moonshot-key", + moonshotBaseUrl: "https://api.moonshot.cn/v1", + }, + } as any, + ) + + // flushModels should have been called for moonshot + const moonshotFlushCalls = flushModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot") + expect(moonshotFlushCalls.length).toBe(1) + expect(moonshotFlushCalls[0][0]).toEqual({ + provider: "moonshot", + apiKey: "new-moonshot-key", + baseUrl: "https://api.moonshot.cn/v1", + }) + + // getModels should use the provided credentials + const moonshotCalls = getModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot") + expect(moonshotCalls.length).toBe(1) + expect(moonshotCalls[0][0]).toEqual({ + provider: "moonshot", + apiKey: "new-moonshot-key", + baseUrl: "https://api.moonshot.cn/v1", + }) + }) + + it("does not flush Moonshot cache when using stored credentials", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { + moonshotApiKey: "stored-moonshot-key", + }, + }) + + getModelsMock.mockImplementation(async (options: any) => { + if (options?.provider === "moonshot") { + return { "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true } } + } + + switch (options?.provider) { + case "openrouter": + return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } } + case "requesty": + return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } + case "vercel-ai-gateway": + return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case "litellm": + return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } + default: + return {} + } + }) + + await webviewMessageHandler( + mockProvider as any, + { + type: "requestRouterModels", + } as any, + ) + + // flushModels should NOT have been called for moonshot + const moonshotFlushCalls = flushModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot") + expect(moonshotFlushCalls.length).toBe(0) + + // getModels should still have been called with stored credentials + const moonshotCalls = getModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot") + expect(moonshotCalls.length).toBe(1) + expect(moonshotCalls[0][0]).toEqual({ + provider: "moonshot", + apiKey: "stored-moonshot-key", + baseUrl: undefined, + }) + }) + + it("posts a Moonshot provider error and keeps an empty aggregate entry when fetch fails", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { + moonshotApiKey: "stored-moonshot-key", + }, + }) + + getModelsMock.mockImplementation(async (options: any) => { + if (options?.provider === "moonshot") { + throw new Error("Moonshot API error") + } + + switch (options?.provider) { + case "openrouter": + return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } } + case "requesty": + return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } + case "vercel-ai-gateway": + return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case "litellm": + return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } + default: + return {} + } + }) + + await webviewMessageHandler( + mockProvider as any, + { + type: "requestRouterModels", + } as any, + ) + + // Should have posted an error for moonshot + const errorCall = (mockProvider.postMessageToWebview as any).mock.calls.find( + (c: any[]) => c[0]?.type === "singleRouterModelFetchResponse" && c[0]?.values?.provider === "moonshot", + ) + expect(errorCall).toBeTruthy() + expect(errorCall[0].success).toBe(false) + + // Aggregate entry should still be empty + const call = (mockProvider.postMessageToWebview as any).mock.calls.find( + (c: any[]) => c[0]?.type === "routerModels", + ) + expect(call).toBeTruthy() + expect(call[0].routerModels.moonshot).toEqual({}) + }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 12832da249..de6bb54c57 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -401,6 +401,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, @@ -587,6 +588,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, @@ -647,6 +649,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": mockModels, kenari: mockModels, }, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c3918e24a5..35bec4e8bb 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1041,6 +1041,7 @@ export const webviewMessageHandler = async ( lmstudio: {}, poe: {}, deepseek: {}, + moonshot: {}, "opencode-go": {}, kenari: {}, } @@ -1136,6 +1137,21 @@ export const webviewMessageHandler = async ( }) } + // Moonshot is conditional on apiKey + const moonshotApiKey = message?.values?.moonshotApiKey ?? apiConfiguration.moonshotApiKey + const moonshotBaseUrl = message?.values?.moonshotBaseUrl ?? apiConfiguration.moonshotBaseUrl + + if (moonshotApiKey) { + if (message?.values?.moonshotApiKey || message?.values?.moonshotBaseUrl) { + await flushModels({ provider: "moonshot", apiKey: moonshotApiKey, baseUrl: moonshotBaseUrl }, true) + } + + candidates.push({ + key: "moonshot", + options: { provider: "moonshot", apiKey: moonshotApiKey, baseUrl: moonshotBaseUrl }, + }) + } + // Opencode Go's /models endpoint is public — it returns the full model list with no // Authorization header — so it's fetched unconditionally like openrouter/vercel-ai-gateway // above. Gating it behind a key meant the picker stayed empty (and fell back to the default diff --git a/src/shared/api.ts b/src/shared/api.ts index d5a579be9d..0cec753591 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -186,6 +186,7 @@ const dynamicProviderExtras = { lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type poe: {} as { apiKey?: string; baseUrl?: string }, deepseek: {} as { apiKey?: string; baseUrl?: string }, + moonshot: {} as { apiKey?: string; baseUrl?: string }, "opencode-go": {} as { apiKey?: string }, kenari: {} as { apiKey?: string }, } as const satisfies Record diff --git a/webview-ui/src/components/settings/providers/Moonshot.tsx b/webview-ui/src/components/settings/providers/Moonshot.tsx index f9ec8e0272..2d6c7d849a 100644 --- a/webview-ui/src/components/settings/providers/Moonshot.tsx +++ b/webview-ui/src/components/settings/providers/Moonshot.tsx @@ -1,13 +1,22 @@ -import { useCallback } from "react" +import { useCallback, useState, useEffect, useRef } from "react" import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" +import { useQueryClient } from "@tanstack/react-query" -import type { ProviderSettings } from "@roo-code/types" +import type { ProviderSettings, ExtensionMessage } from "@roo-code/types" +import { moonshotDefaultModelId } from "@roo-code/types" + +import { RouterName } from "@roo/api" import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useExtensionState } from "@src/context/ExtensionStateContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { vscode } from "@src/utils/vscode" +import { Button } from "@src/components/ui" +import { ModelPicker } from "../ModelPicker" +import { handleModelChangeSideEffects } from "../utils/providerModelConfig" +import type { ProviderName } from "@roo-code/types" import { inputEventTransform } from "../transforms" -import { cn } from "@/lib/utils" type MoonshotProps = { apiConfiguration: ProviderSettings @@ -15,8 +24,39 @@ type MoonshotProps = { simplifySettings?: boolean } -export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: MoonshotProps) => { +export const Moonshot = ({ apiConfiguration, setApiConfigurationField, simplifySettings }: MoonshotProps) => { const { t } = useAppTranslation() + const { routerModels } = useExtensionState() + const queryClient = useQueryClient() + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const moonshotErrorJustReceived = useRef(false) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "singleRouterModelFetchResponse" && !message.success) { + const providerName = message.values?.provider as RouterName + if (providerName === "moonshot" && refreshStatus === "loading") { + moonshotErrorJustReceived.current = true + setRefreshStatus("error") + setRefreshError(message.error) + } + } else if (message.type === "routerModels") { + if (refreshStatus === "loading") { + if (!moonshotErrorJustReceived.current) { + setRefreshStatus("success") + queryClient.invalidateQueries({ queryKey: ["routerModels"] }) + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus, queryClient]) const handleInputChange = useCallback( ( @@ -29,6 +69,25 @@ export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: Moonsho [setApiConfigurationField], ) + const handleRefreshModels = useCallback(() => { + moonshotErrorJustReceived.current = false + setRefreshStatus("loading") + setRefreshError(undefined) + + const key = apiConfiguration.moonshotApiKey + + if (!key) { + setRefreshStatus("error") + setRefreshError(t("settings:providers.refreshModels.missingConfig")) + return + } + + vscode.postMessage({ + type: "requestRouterModels", + values: { moonshotApiKey: key, moonshotBaseUrl: apiConfiguration.moonshotBaseUrl }, + }) + }, [apiConfiguration, t]) + return ( <>
@@ -36,7 +95,7 @@ export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: Moonsho + className="w-full"> api.moonshot.ai @@ -69,6 +128,45 @@ export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: Moonsho )}
+ + handleModelChangeSideEffects("moonshot" as ProviderName, modelId, setApiConfigurationField) + } + /> + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading")} +
+ )} + {refreshStatus === "success" && ( +
{t("settings:providers.refreshModels.success")}
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error")} +
+ )} ) } diff --git a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx new file mode 100644 index 0000000000..1ffcf9b683 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx @@ -0,0 +1,397 @@ +// npx vitest src/components/settings/providers/__tests__/Moonshot.spec.tsx + +import React from "react" +import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" +import type { ProviderSettings } from "@roo-code/types" + +import { Moonshot } from "../Moonshot" + +// Mock the translation hook +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +// Mock VSCode webview toolkit components using importOriginal +vi.mock("@vscode/webview-ui-toolkit/react", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + VSCodeTextField: ({ children, value, onInput, type, placeholder }: any) => ( +
+ {children} + onInput?.(e)} + placeholder={placeholder} + data-testid="moonshot-api-key-input" + /> +
+ ), + VSCodeDropdown: ({ children, value, onChange }: any) => ( +
+ {children} + +
+ ), + VSCodeOption: ({ children, value }: any) => ( +
+ {children} +
+ ), + } +}) + +// Mock the ModelPicker - must be a simple component that doesn't import anything +vi.mock("../ModelPicker", () => ({ + ModelPicker: function MockModelPicker() { + return React.createElement( + "div", + { "data-testid": "model-picker" }, + React.createElement("span", { "data-testid": "model-picker-default" }, "mock-default"), + React.createElement("span", { "data-testid": "model-picker-count" }, "0"), + ) + }, +})) + +// Mock vscode - factory must not reference outer scope variables +vi.mock("@src/utils/vscode", () => { + const mockPostMessage = vi.fn() + return { + vscode: { postMessage: mockPostMessage }, + } +}) + +// Mock useExtensionState +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: vi.fn(), +})) + +// Mock react-use +vi.mock("react-use", () => ({ + useEvent: vi.fn(), +})) + +// Mock useRouterModels +vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: () => ({ data: {}, isLoading: false, error: null }), +})) + +// Mock @src/components/ui using importOriginal to get all real exports +vi.mock("@src/components/ui", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + Button: ({ children, onClick, disabled, variant }: any) => ( + + ), + } +}) + +// Mock VSCodeButtonLink +vi.mock("@src/components/common/VSCodeButtonLink", () => ({ + VSCodeButtonLink: ({ href, children }: any) => ( + + {children} + + ), +})) + +// Mock handleModelChangeSideEffects +vi.mock("../utils/providerModelConfig", () => ({ + handleModelChangeSideEffects: vi.fn(), +})) + +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +const mockUseExtensionState = useExtensionState as ReturnType + +describe("Moonshot Component", () => { + const mockSetApiConfigurationField = vi.fn() + + const createDefaultApiConfiguration = (overrides?: Partial): ProviderSettings => ({ + apiProvider: "moonshot", + moonshotBaseUrl: "https://api.moonshot.ai/v1", + ...overrides, + }) + + beforeEach(() => { + vi.clearAllMocks() + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + }) + + it("renders API key input, base URL dropdown, and refresh button", () => { + render( + , + ) + + expect(screen.getByTestId("moonshot-api-key-input")).toBeInTheDocument() + expect(screen.getByTestId("vscode-dropdown")).toBeInTheDocument() + // Use getAllByTestId and filter by variant to find the refresh button + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline") + expect(refreshButton).toBeInTheDocument() + }) + + it("refresh button is disabled when no API key is provided", () => { + render( + , + ) + + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline") + expect(refreshButton).toBeDisabled() + }) + + it("refresh button is enabled when API key is provided", () => { + render( + , + ) + + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline") + expect(refreshButton).not.toBeDisabled() + }) + + it("shows loading state when refresh is clicked", () => { + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + + render( + , + ) + + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline")! + fireEvent.click(refreshButton) + + // After click, the button should be disabled (loading state) + expect(refreshButton).toBeDisabled() + expect(screen.getByText("settings:providers.refreshModels.loading")).toBeInTheDocument() + }) + + it("shows success state after routerModels message received", async () => { + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + + render( + , + ) + + // Click refresh to enter loading state + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline")! + fireEvent.click(refreshButton) + + // Simulate receiving routerModels message (without prior error) + window.postMessage( + { + type: "routerModels", + values: { moonshot: { "kimi-k2-0905-preview": { maxTokens: 16384 } } }, + }, + "*", + ) + + await waitFor(() => { + expect(screen.getByText("settings:providers.refreshModels.success")).toBeInTheDocument() + }) + }) + + it("shows error state after singleRouterModelFetchResponse error message", async () => { + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + + render( + , + ) + + // Click refresh to enter loading state + await act(async () => { + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline") + fireEvent.click(refreshButton!) + }) + + // Wait for loading state to render and effect to re-run with new refreshStatus + await waitFor(() => { + expect(screen.getByText("settings:providers.refreshModels.loading")).toBeInTheDocument() + }) + + // Simulate error message — use setTimeout to ensure the useEffect has + // re-run with the updated refreshStatus === "loading" closure before the + // message event fires, otherwise the handler closure still captures "idle". + await act(async () => { + return new Promise((resolve) => { + setTimeout(() => { + window.postMessage( + { + type: "singleRouterModelFetchResponse", + success: false, + error: "API connection failed", + values: { provider: "moonshot" }, + }, + "*", + ) + resolve(undefined) + }, 0) + }) + }) + + // The component displays refreshError ("API connection failed") when set, + // falling back to the generic message only when refreshError is undefined. + await waitFor(() => { + expect(screen.getByText("API connection failed")).toBeInTheDocument() + }) + }) + + it("race condition: error arrives before routerModels success — stays in error state", async () => { + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + + render( + , + ) + + // Click refresh to enter loading state + await act(async () => { + const buttons = screen.getAllByTestId("button") + const btn = buttons.find((b) => b.getAttribute("data-variant") === "outline") + fireEvent.click(btn!) + }) + + // Wait for loading state to render + await waitFor(() => { + expect(screen.getByText("settings:providers.refreshModels.loading")).toBeInTheDocument() + }) + + // Error arrives first — delay with setTimeout so the handler closure + // captures refreshStatus === "loading". + await act(async () => { + return new Promise((resolve) => { + setTimeout(() => { + window.postMessage( + { + type: "singleRouterModelFetchResponse", + success: false, + error: "API connection failed", + values: { provider: "moonshot" }, + }, + "*", + ) + resolve(undefined) + }, 0) + }) + }) + + // The component displays refreshError ("API connection failed") when set. + await waitFor(() => { + expect(screen.getByText("API connection failed")).toBeInTheDocument() + }) + + // Then routerModels arrives — should NOT flip to success because error was received + await act(async () => { + window.postMessage( + { + type: "routerModels", + values: { moonshot: { "kimi-k2-0905-preview": { maxTokens: 16384 } } }, + }, + "*", + ) + }) + + // Error state should still be present (not flipped to success) + await waitFor(() => { + expect(screen.getByText("API connection failed")).toBeInTheDocument() + }) + }) + + it("sends correct message when refresh is clicked", () => { + mockUseExtensionState.mockReturnValue({ + routerModels: {}, + }) + + render( + , + ) + + const buttons = screen.getAllByTestId("button") + const refreshButton = buttons.find((b) => b.getAttribute("data-variant") === "outline")! + fireEvent.click(refreshButton) + + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "requestRouterModels", + values: { + moonshotApiKey: "test-key", + moonshotBaseUrl: "https://api.moonshot.cn/v1", + }, + }), + ) + }) + + it("shows 'Get Moonshot API Key' link when no API key is set", () => { + render( + , + ) + + expect(screen.getByTestId("vscode-button-link")).toBeInTheDocument() + expect(screen.getByTestId("vscode-button-link")).toHaveAttribute( + "href", + "https://platform.moonshot.ai/console/api-keys", + ) + }) + + it("hides 'Get Moonshot API Key' link when API key is set", () => { + render( + , + ) + + expect(screen.queryByTestId("vscode-button-link")).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index d4c87ec02c..ed3df0209d 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -206,6 +206,7 @@ export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [ "ollama", "lmstudio", "vscode-lm", + "moonshot", // Moonshot has custom ModelPicker inside Moonshot.tsx ] /** diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 9f107b3841..9433bb8148 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -20,6 +20,8 @@ import { openRouterDefaultModelId, vscodeLlmModels, vscodeLlmDefaultModelId, + moonshotDefaultModelId, + moonshotModels, } from "@roo-code/types" import { useSelectedModel } from "../useSelectedModel" @@ -1041,4 +1043,99 @@ describe("useSelectedModel", () => { expect(result.current.info).toEqual(friendliModels["zai-org/GLM-5.1"]) }) }) + + describe("moonshot provider", () => { + beforeEach(() => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + }) + + it("should return default moonshot model when no custom model is specified", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: "moonshot", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("moonshot") + expect(result.current.id).toBe(moonshotDefaultModelId) + expect(result.current.info).toEqual(moonshotModels[moonshotDefaultModelId]) + }) + + it("should use router model override when routerModels.moonshot has the model", () => { + const routerModelInfo: ModelInfo = { + maxTokens: 32000, + contextWindow: 262144, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 1.0, + outputPrice: 5.0, + } + + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: { + "kimi-k2-0905-preview": routerModelInfo, + }, + }, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "moonshot", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.id).toBe(moonshotDefaultModelId) + // Router info takes precedence over static info + expect(result.current.info).toEqual(routerModelInfo) + }) + + it("should fallback to default when model ID is not in static or router models", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: "moonshot", + apiModelId: "non-existent-model", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.id).toBe(moonshotDefaultModelId) + expect(result.current.info).toEqual(moonshotModels[moonshotDefaultModelId]) + }) + + it("should use getValidatedModelId to return apiModelId when valid", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: "moonshot", + apiModelId: "kimi-k2-turbo-preview", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.id).toBe("kimi-k2-turbo-preview") + expect(result.current.info).toEqual(moonshotModels["kimi-k2-turbo-preview"]) + }) + }) }) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 62a30a26b2..93dca94a24 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -253,9 +253,13 @@ function getSelectedModel({ return { id, info: routerInfo ?? staticInfo } } case "moonshot": { - const id = apiConfiguration.apiModelId ?? defaultModelId - const info = moonshotModels[id as keyof typeof moonshotModels] - return { id, info } + const availableModels = routerModels.moonshot + ? { ...moonshotModels, ...routerModels.moonshot } + : moonshotModels + const id = getValidatedModelId(apiConfiguration.apiModelId, availableModels, defaultModelId) + const routerInfo = routerModels.moonshot?.[id] + const staticInfo = moonshotModels[id as keyof typeof moonshotModels] + return { id, info: routerInfo ?? staticInfo } } case "minimax": { const id = apiConfiguration.apiModelId ?? defaultModelId diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 04484397c6..700bf27891 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -54,6 +54,7 @@ describe("Model Validation Functions", () => { "opencode-go": {}, kenari: {}, "zoo-gateway": {}, + moonshot: {}, } const allowAllOrganization: OrganizationAllowList = { From 3024768cf9bf214fc3d65747068c77661127188f Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:49:14 +0000 Subject: [PATCH 41/89] feat(gemini): add Gemini 3.6 Flash support (#975) Co-authored-by: Roomote --- packages/types/src/providers/gemini.ts | 13 +++++++++++++ packages/types/src/providers/vertex.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/types/src/providers/gemini.ts b/packages/types/src/providers/gemini.ts index b26faa39bd..e180be0867 100644 --- a/packages/types/src/providers/gemini.ts +++ b/packages/types/src/providers/gemini.ts @@ -6,6 +6,19 @@ export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-3.1-pro-preview" export const geminiModels = { + "gemini-3.6-flash": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high"], + reasoningEffort: "medium", + inputPrice: 1.5, + outputPrice: 7.5, + cacheReadsPrice: 0.15, + cacheWritesPrice: 1.0, + supportsReasoningBudget: false, + }, "gemini-3.5-flash": { maxTokens: 65_536, contextWindow: 1_048_576, diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index dc25191543..d6a156e3fe 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -6,6 +6,19 @@ export type VertexModelId = keyof typeof vertexModels export const vertexDefaultModelId: VertexModelId = "claude-sonnet-4-5@20250929" export const vertexModels = { + "gemini-3.6-flash": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high"], + reasoningEffort: "medium", + inputPrice: 1.5, + outputPrice: 7.5, + cacheReadsPrice: 0.15, + cacheWritesPrice: 1.0, + supportsReasoningBudget: false, + }, "gemini-3.5-flash": { maxTokens: 65_536, contextWindow: 1_048_576, From cc2588be4399a64f245afab053e4ac3758a69b68 Mon Sep 17 00:00:00 2001 From: dw <41457565+daewoongoh@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:56:29 +0900 Subject: [PATCH 42/89] fix(litellm): preserve reasoning_content for known reasoning model families (#899) * fix(litellm): preserve reasoning_content for known reasoning model families LiteLLM's /v1/model/info never reports reasoning capability flags, so infer preserveReasoning from the model alias/routed-model name and use convertToR1Format to keep reasoning_content intact across tool-call turns. Signed-off-by: daewoongoh * test(litellm): cover preserveReasoning inference and message conversion branching Add unit coverage for LITELLM_PRESERVE_REASONING_PATTERN matching per model family, getLiteLLMModels setting preserveReasoning on matched models, and LiteLLMHandler.createMessage branching between convertToR1Format and convertToOpenAiMessages based on info.preserveReasoning. Signed-off-by: daewoongoh * refactor(litellm): replace preserveReasoning regex with an explicit model id list Regex-based family matching could over-match unrelated aliases sharing a substring (e.g. glm-5-flash matching a glm-5 prefix), a gap flagged in PR #899 review. LITELLM_PRESERVE_REASONING_MODEL_IDS now lists the exact model ids that set preserveReasoning: true in their native provider config, matched via isLiteLLMPreserveReasoningModel() against the final segment of model_name/litellm_params.model. Also fixes a test in litellm.spec.ts where the model_name alias itself matched a known id, so the assertion didn't actually prove litellm_params.model was being checked. Signed-off-by: daewoongoh --------- Signed-off-by: daewoongoh --- packages/types/src/__tests__/lite-llm.test.ts | 47 ++++++ packages/types/src/providers/lite-llm.ts | 80 ++++++++++ src/api/providers/__tests__/lite-llm.spec.ts | 139 ++++++++++++++++++ .../fetchers/__tests__/litellm.spec.ts | 113 ++++++++++++++ src/api/providers/fetchers/litellm.ts | 7 + src/api/providers/lite-llm.ts | 17 ++- 6 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 packages/types/src/__tests__/lite-llm.test.ts diff --git a/packages/types/src/__tests__/lite-llm.test.ts b/packages/types/src/__tests__/lite-llm.test.ts new file mode 100644 index 0000000000..03424b2e57 --- /dev/null +++ b/packages/types/src/__tests__/lite-llm.test.ts @@ -0,0 +1,47 @@ +import { isLiteLLMPreserveReasoningModel, LITELLM_PRESERVE_REASONING_MODEL_IDS } from "../providers/lite-llm.js" + +describe("LiteLLM preserveReasoning model detection", () => { + it("matches every explicitly listed model id", () => { + for (const modelId of LITELLM_PRESERVE_REASONING_MODEL_IDS) { + expect(isLiteLLMPreserveReasoningModel(modelId)).toBe(true) + } + }) + + it("does not contain duplicate model ids", () => { + expect(new Set(LITELLM_PRESERVE_REASONING_MODEL_IDS).size).toBe(LITELLM_PRESERVE_REASONING_MODEL_IDS.length) + }) + + it("matches provider-prefixed routed model names by their final segment", () => { + expect(isLiteLLMPreserveReasoningModel("deepseek/deepseek-reasoner")).toBe(true) + expect(isLiteLLMPreserveReasoningModel("bedrock/moonshot.kimi-k2-thinking")).toBe(true) + expect(isLiteLLMPreserveReasoningModel("fireworks_ai/accounts/fireworks/models/kimi-k2p7-code")).toBe(true) + }) + + it("matches case-insensitively", () => { + expect(isLiteLLMPreserveReasoningModel("MiniMax-M2.7-Highspeed")).toBe(true) + expect(isLiteLLMPreserveReasoningModel("GLM-5.2")).toBe(true) + }) + + it("does not match model ids that merely contain a known family as a substring", () => { + expect(isLiteLLMPreserveReasoningModel("deepseek-v4-mini")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("mimo-v2.6")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("kimi-k2.6")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("kimi-k2.7-code")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("minimax-m4")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("minimax-m1")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("glm-4.7-flash")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("glm-4.7-flashx")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("glm-5-flash")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("glm-4.8")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("qwen3.5-plus")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("qwen3.7-mini")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("qwen3.6-max")).toBe(false) + }) + + it("does not match unrelated model names", () => { + expect(isLiteLLMPreserveReasoningModel("gpt-4")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("claude-3-opus")).toBe(false) + expect(isLiteLLMPreserveReasoningModel("")).toBe(false) + expect(isLiteLLMPreserveReasoningModel(undefined)).toBe(false) + }) +}) diff --git a/packages/types/src/providers/lite-llm.ts b/packages/types/src/providers/lite-llm.ts index 14a68cfc3c..a63f3971f5 100644 --- a/packages/types/src/providers/lite-llm.ts +++ b/packages/types/src/providers/lite-llm.ts @@ -13,3 +13,83 @@ export const litellmDefaultModelInfo: ModelInfo = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, } + +/** + * LiteLLM is a gateway: it fronts arbitrary underlying models and its + * `/v1/model/info` response carries no reasoning-related capability flags + * (no `preserveReasoning` equivalent). The underlying model identity is only + * visible as text in the model alias (`model_name`) or the routed target + * (`litellm_params.model`, e.g. `deepseek/deepseek-reasoner`, + * `bedrock/moonshot.kimi-k2-thinking`, `fireworks_ai/.../kimi-k2p7-code`). + * + * Rather than matching model-family substrings with a regex (which can + * over-match unrelated aliases, e.g. a family fragment appearing inside a + * longer unrelated model id), this is an explicit list of the exact model + * ids that set `preserveReasoning: true` in their native provider config + * (see deepseek.ts, mimo.ts, moonshot.ts, bedrock.ts, fireworks.ts, zai.ts, + * minimax.ts, opencode-go.ts). The same behavior is inferred for a + * LiteLLM-routed alias of the same underlying model. Keep this list in sync + * with those registries. This is still best-effort: unrecognized aliases or + * renamed deployments will not match, and callers should treat it as a + * heuristic, not a source of truth. + */ +export const LITELLM_PRESERVE_REASONING_MODEL_IDS = [ + // deepseek.ts + "deepseek-v4-flash", + "deepseek-v4-pro", + "deepseek-reasoner", + + // mimo.ts, opencode-go.ts + "mimo-v2.5", + "mimo-v2.5-pro", + + // moonshot.ts, bedrock.ts, fireworks.ts + "kimi-k2-thinking", + "moonshot.kimi-k2-thinking", + "kimi-k2p7-code", + + // zai.ts + "glm-4.7", + "glm-5", + "glm-5.1", + "glm-5.2", + "glm-5-turbo", + + // bedrock.ts, minimax.ts, opencode-go.ts + "minimax.minimax-m2", + "minimax-m2", + "minimax-m2-stable", + "minimax-m2.1", + "minimax-m2.1-highspeed", + "minimax-m2.5", + "minimax-m2.5-highspeed", + "minimax-m2.7", + "minimax-m2.7-highspeed", + "minimax-m3", + + // opencode-go.ts + "qwen3.6-plus", + "qwen3.7-plus", + "qwen3.7-max", +] as const + +const LITELLM_PRESERVE_REASONING_MODEL_ID_SET = new Set(LITELLM_PRESERVE_REASONING_MODEL_IDS) + +/** + * Checks whether `modelName` (a LiteLLM alias or routed `litellm_params.model` + * value) identifies a model that requires `preserveReasoning: true`. + * Provider-prefixed routed names (e.g. `deepseek/deepseek-reasoner`, + * `fireworks_ai/accounts/fireworks/models/kimi-k2p7-code`) are matched by + * their final slash-delimited segment. + */ +export function isLiteLLMPreserveReasoningModel(modelName: string | undefined): boolean { + const normalized = modelName?.trim().toLowerCase() + + if (!normalized) { + return false + } + + const modelId = normalized.split("/").pop() + + return modelId !== undefined && LITELLM_PRESERVE_REASONING_MODEL_ID_SET.has(modelId) +} diff --git a/src/api/providers/__tests__/lite-llm.spec.ts b/src/api/providers/__tests__/lite-llm.spec.ts index ab2f261058..a5898513c5 100644 --- a/src/api/providers/__tests__/lite-llm.spec.ts +++ b/src/api/providers/__tests__/lite-llm.spec.ts @@ -1124,6 +1124,145 @@ describe("LiteLLMHandler", () => { }) }) + describe("preserveReasoning message conversion", () => { + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + choices: [{ delta: { content: "ok" } }], + usage: { prompt_tokens: 1, completion_tokens: 1 }, + } + }, + } + + it("uses convertToR1Format (merging tool-result text) when the model info sets preserveReasoning", async () => { + const optionsWithReasoning: ApiHandlerOptions = { + ...mockOptions, + litellmModelId: "deepseek-reasoner-alias", + } + handler = new LiteLLMHandler(optionsWithReasoning) + + vi.spyOn(handler as any, "fetchModel").mockResolvedValue({ + id: "deepseek-reasoner-alias", + info: { ...litellmDefaultModelInfo, preserveReasoning: true }, + }) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [ + { type: "text", text: "I'll help." }, + { type: "tool_use", id: "toolu_123", name: "read_file", input: { path: "test.txt" } }, + ], + // DeepSeek-style interleaved thinking: must be echoed back in the next request. + reasoning_content: "Let me check the file first.", + } as Anthropic.Messages.MessageParam & { reasoning_content: string }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "toolu_123", content: "file contents" }, + { type: "text", text: "Thanks, continue." }, + ], + }, + ] + + mockCreate.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const generator = handler.createMessage(systemPrompt, messages) + for await (const _chunk of generator) { + // Consume + } + + const createCall = mockCreate.mock.calls[0][0] + + // convertToR1Format with mergeToolResultText folds the trailing text into the + // tool message instead of appending a separate user message. + const toolMessage = createCall.messages.find((msg: any) => msg.role === "tool") + expect(toolMessage).toBeDefined() + expect(toolMessage.content).toBe("file contents\n\nThanks, continue.") + + const trailingUserMessage = createCall.messages.find( + (msg: any) => msg.role === "user" && msg.content === "Thanks, continue.", + ) + expect(trailingUserMessage).toBeUndefined() + + // The whole point of routing through convertToR1Format: reasoning_content + // must survive on the assistant message so the model doesn't reject the + // follow-up request for missing prior reasoning. + const assistantMessage = createCall.messages.find( + (msg: any) => msg.role === "assistant" && msg.tool_calls?.length > 0, + ) + expect(assistantMessage).toBeDefined() + expect(assistantMessage.reasoning_content).toBe("Let me check the file first.") + }) + + it("uses convertToOpenAiMessages (no merging) when the model info does not set preserveReasoning", async () => { + vi.spyOn(handler as any, "fetchModel").mockResolvedValue({ + id: litellmDefaultModelId, + info: { ...litellmDefaultModelInfo, preserveReasoning: undefined }, + }) + + const systemPrompt = "You are a helpful assistant" + // Task.buildCleanConversationHistory() (src/core/task/Task.ts) already strips the + // reasoning content block before messages reach this handler when the model's + // preserveReasoning is not true, so no reasoning_content/reasoning block is present + // on the assistant message here — this input reflects what the handler actually + // receives in that case. + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [ + { type: "text", text: "I'll help." }, + { type: "tool_use", id: "toolu_123", name: "read_file", input: { path: "test.txt" } }, + ], + }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "toolu_123", content: "file contents" }, + { type: "text", text: "Thanks, continue." }, + ], + }, + ] + + mockCreate.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const generator = handler.createMessage(systemPrompt, messages) + for await (const _chunk of generator) { + // Consume + } + + const createCall = mockCreate.mock.calls[0][0] + + const toolMessage = createCall.messages.find((msg: any) => msg.role === "tool") + expect(toolMessage).toBeDefined() + expect(toolMessage.content).toBe("file contents") + + const trailingUserMessage = createCall.messages.find( + (msg: any) => + msg.role === "user" && + (msg.content === "Thanks, continue." || + (Array.isArray(msg.content) && + msg.content.some((part: any) => part.text === "Thanks, continue."))), + ) + expect(trailingUserMessage).toBeDefined() + + // No reasoning_content is sent to the API in this branch, matching what + // buildCleanConversationHistory already stripped upstream. + const assistantMessage = createCall.messages.find( + (msg: any) => msg.role === "assistant" && msg.tool_calls?.length > 0, + ) + expect(assistantMessage).toBeDefined() + expect(assistantMessage.reasoning_content).toBeUndefined() + }) + }) + describe("session ID header", () => { const mockStream = { async *[Symbol.asyncIterator]() { diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index c05cda8839..8e6d49ddae 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -697,4 +697,117 @@ describe("getLiteLLMModels", () => { description: "model-with-only-max-output-tokens via LiteLLM proxy", }) }) + + describe("preserveReasoning inference", () => { + it("sets preserveReasoning: true when only the routed model (not the alias) matches a known reasoning model id", async () => { + const mockResponse = { + data: { + data: [ + { + model_name: "my-deepseek-alias", + model_info: { + max_tokens: 8192, + max_input_tokens: 128000, + }, + litellm_params: { + model: "deepseek/deepseek-reasoner", + }, + }, + { + model_name: "my-kimi-alias", + model_info: { + max_tokens: 8192, + max_input_tokens: 128000, + }, + litellm_params: { + model: "bedrock/moonshot.kimi-k2-thinking", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") + + expect(result["my-deepseek-alias"]).toMatchObject({ preserveReasoning: true }) + expect(result["my-kimi-alias"]).toMatchObject({ preserveReasoning: true }) + }) + + it("omits preserveReasoning when the routed model does not match a known reasoning model id", async () => { + const mockResponse = { + data: { + data: [ + { + model_name: "gpt-4-turbo", + model_info: { + max_tokens: 8192, + max_input_tokens: 128000, + }, + litellm_params: { + model: "openai/gpt-4-turbo", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") + + expect(result["gpt-4-turbo"]).not.toHaveProperty("preserveReasoning") + }) + + it("matches against the model alias even when the routed model name does not match", async () => { + const mockResponse = { + data: { + data: [ + { + model_name: "glm-5.2", + model_info: { + max_tokens: 8192, + max_input_tokens: 128000, + }, + litellm_params: { + model: "zai/some-custom-deployment", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") + + expect(result["glm-5.2"]).toMatchObject({ preserveReasoning: true }) + }) + + it("does not match a model id that merely contains a known family as a substring", async () => { + const mockResponse = { + data: { + data: [ + { + model_name: "glm-5-flash", + model_info: { + max_tokens: 8192, + max_input_tokens: 128000, + }, + litellm_params: { + model: "zai/glm-5-flash", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") + + expect(result["glm-5-flash"]).not.toHaveProperty("preserveReasoning") + }) + }) }) diff --git a/src/api/providers/fetchers/litellm.ts b/src/api/providers/fetchers/litellm.ts index de895d01fb..83373e5163 100644 --- a/src/api/providers/fetchers/litellm.ts +++ b/src/api/providers/fetchers/litellm.ts @@ -1,6 +1,7 @@ import axios from "axios" import type { ModelRecord } from "@roo-code/types" +import { isLiteLLMPreserveReasoningModel } from "@roo-code/types" import { DEFAULT_HEADERS } from "../constants" /** @@ -40,6 +41,11 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise if (!modelName || !modelInfo || !litellmModelName) continue + // LiteLLM's /v1/model/info never reports reasoning capability flags, so infer + // preserveReasoning from explicit model ids in either the alias or routed model name. + const preservesReasoning = + isLiteLLMPreserveReasoningModel(modelName) || isLiteLLMPreserveReasoningModel(litellmModelName) + models[modelName] = { maxTokens: modelInfo.max_output_tokens || modelInfo.max_tokens || 8192, contextWindow: modelInfo.max_input_tokens || 200000, @@ -55,6 +61,7 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise cacheReadsPrice: modelInfo.cache_read_input_token_cost ? modelInfo.cache_read_input_token_cost * 1000000 : undefined, + ...(preservesReasoning && { preserveReasoning: true }), description: `${modelName} via LiteLLM proxy`, } } diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index 49cc1b115b..74a610b073 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -9,6 +9,7 @@ import { ApiHandlerOptions } from "../../shared/api" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" +import { convertToR1Format } from "../transform/r1-format" import { GEMINI_THOUGHT_SIGNATURE_BYPASS } from "../transform/gemini-format" import { sanitizeOpenAiCallId } from "../../utils/tool-id" @@ -117,9 +118,19 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa ): ApiStream { const { id: modelId, info } = await this.fetchModel() - const openAiMessages = convertToOpenAiMessages(messages, { - normalizeToolCallId: sanitizeOpenAiCallId, - }) + // Models that require reasoning_content to be echoed back during tool-call + // continuations (see LITELLM_PRESERVE_REASONING_MODEL_IDS) need convertToR1Format: + // it merges consecutive same-role messages and, via mergeToolResultText, folds + // text following tool_results into the last tool message so a user message + // never gets inserted mid-turn and causes the model to drop prior reasoning_content. + const openAiMessages = info.preserveReasoning + ? convertToR1Format(messages, { + normalizeToolCallId: sanitizeOpenAiCallId, + mergeToolResultText: true, + }) + : convertToOpenAiMessages(messages, { + normalizeToolCallId: sanitizeOpenAiCallId, + }) // Prepare messages with cache control if enabled and supported let systemMessage: OpenAI.Chat.ChatCompletionMessageParam From 71a58a16cb61172c77e75b3b60ab51e475636658 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:05:10 -0400 Subject: [PATCH 43/89] chore(deps): update dependency esbuild-wasm to v0.28.1 (#829) * chore(deps): update dependency esbuild-wasm to v0.28.1 * fix(build): eagerly initialize stdout/stderr in esbuild-wasm to prevent V8 crash --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- packages/build/src/esbuild.ts | 14 +++++++++++--- pnpm-lock.yaml | 18 ++++++------------ src/package.json | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/build/src/esbuild.ts b/packages/build/src/esbuild.ts index 952e823eec..b2183d6253 100644 --- a/packages/build/src/esbuild.ts +++ b/packages/build/src/esbuild.ts @@ -197,15 +197,23 @@ function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void { ] for (const { src, dest } of filesToCopy) { - fs.copyFileSync(src, dest) - - // Make CLI executable. if (src.endsWith("esbuild")) { + // Read the esbuild binary, patch it to prevent the V8 crash (crbug.com/1201626) + // by eagerly initializing process.stdout.fd and process.stderr.fd on line 2 + // before WebAssembly execution takes place. + let content = fs.readFileSync(src, "utf8") + content = content.replace( + /^(#!.*$)/m, + "$1\n// Eagerly initialize stdout/stderr to avoid V8 crash (crbug.com/1201626) inside WASM stack\nconst _ = [process.stdout.fd, process.stderr.fd];", + ) + fs.writeFileSync(dest, content) try { fs.chmodSync(dest, 0o755) } catch { // Ignore chmod errors on Windows. } + } else { + fs.copyFileSync(src, dest) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1aa321660e..8e7150a577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -675,8 +675,8 @@ importers: specifier: 6.0.218 version: 6.0.218(zod@3.25.76) esbuild-wasm: - specifier: 0.25.12 - version: 0.25.12 + specifier: 0.28.1 + version: 0.28.1 execa: specifier: 9.6.1 version: 9.6.1 @@ -862,7 +862,7 @@ importers: version: 3.4.1 source-map: specifier: ^0.7.4 - version: 0.7.4 + version: 0.7.6 stacktrace-js: specifier: ^2.0.2 version: 2.0.2 @@ -4555,8 +4555,8 @@ packages: es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - esbuild-wasm@0.25.12: - resolution: {integrity: sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==} + esbuild-wasm@0.28.1: + resolution: {integrity: sha512-p/GD4E8oYRjg3kjdKrnMb0s4PzXgJF42e0MF4H0+ACyK/kIlFRp3e0fzOleIG+wBBm6MM3XQrbpe7soEA+vJIA==} engines: {node: '>=18'} hasBin: true @@ -7360,10 +7360,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -12374,7 +12370,7 @@ snapshots: es6-error@4.1.1: {} - esbuild-wasm@0.25.12: {} + esbuild-wasm@0.28.1: {} esbuild@0.28.1: optionalDependencies: @@ -15826,8 +15822,6 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.4: {} - source-map@0.7.6: {} space-separated-tokens@1.1.5: {} diff --git a/src/package.json b/src/package.json index 7eb12d5fde..18b415580c 100644 --- a/src/package.json +++ b/src/package.json @@ -542,7 +542,7 @@ "@vitest/coverage-v8": "4.1.9", "@vscode/vsce": "3.9.2", "ai": "6.0.218", - "esbuild-wasm": "0.25.12", + "esbuild-wasm": "0.28.1", "execa": "9.6.1", "mkdirp": "3.0.1", "nock": "14.0.15", From badb82c56ff842806b4a26cbfc42a1d262f79254 Mon Sep 17 00:00:00 2001 From: eason liang <136036952+easonLiangWorldedtech@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:06:41 +0800 Subject: [PATCH 44/89] fix(webview): sync cachedState on mode changes in SettingsView (#925) * fix(webview): sync cachedState on mode changes in SettingsView * test(settings): strengthen mode sync change detection --------- Co-authored-by: Eason Liang --- .../src/components/settings/SettingsView.tsx | 12 +- .../SettingsView.change-detection.spec.tsx | 323 ++++++++++++++++-- 2 files changed, 300 insertions(+), 35 deletions(-) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 87052a21a2..0b53342133 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -130,7 +130,7 @@ const SettingsView = forwardRef(({ onDone, t const { t } = useAppTranslation() const extensionState = useExtensionState() - const { currentApiConfigName, listApiConfigMeta, uriScheme, settingsImportedAt } = extensionState + const { currentApiConfigName, listApiConfigMeta, uriScheme, settingsImportedAt, mode } = extensionState const [isDiscardDialogShow, setDiscardDialogShow] = useState(false) const [isChangeDetected, setChangeDetected] = useState(false) @@ -147,6 +147,7 @@ const SettingsView = forwardRef(({ onDone, t const contentRef = useRef(null) const prevApiConfigName = useRef(currentApiConfigName) + const prevMode = useRef(mode) const handledSettingsImportedAt = useRef(undefined) const confirmDialogHandler = useRef<() => void>() @@ -220,16 +221,17 @@ const SettingsView = forwardRef(({ onDone, t const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) useEffect(() => { - // Update only when currentApiConfigName is changed. - // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration. - if (prevApiConfigName.current === currentApiConfigName) { + // Update when currentApiConfigName or mode changes. + // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration or mode switch. + if (prevApiConfigName.current === currentApiConfigName && prevMode.current === mode) { return } setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName + prevMode.current = mode setChangeDetected(false) - }, [currentApiConfigName, extensionState]) + }, [currentApiConfigName, mode, extensionState]) // Bust the cache when settings are imported. useEffect(() => { diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index 2ca7107335..68a6efb106 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -1,5 +1,5 @@ import { act, render, screen, fireEvent, waitFor, configure } from "@testing-library/react" -import { vi, describe, it, expect, beforeEach } from "vitest" +import { vi, describe, it, expect, beforeEach, beforeAll } from "vitest" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import React from "react" @@ -19,8 +19,6 @@ vi.mock("@src/utils/vscode", () => ({ }, })) -// Import the actual component -import SettingsView from "../SettingsView" import { useExtensionState } from "@src/context/ExtensionStateContext" // Mock the extension state context @@ -28,10 +26,12 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: vi.fn(), })) +const mockTranslate = vi.hoisted(() => (key: string) => key) + // Mock the translation context vi.mock("@src/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ - t: (key: string) => key, + t: mockTranslate, }), })) @@ -94,6 +94,18 @@ vi.mock("@src/components/ui", () => ({ TooltipProvider: ({ children }: any) => <>{children}, TooltipTrigger: ({ children }: any) => <>{children}, TooltipContent: ({ children }: any) =>
{children}
, + Command: ({ children }: any) =>
{children}
, + CommandInput: ({ value, onValueChange }: any) => ( + onValueChange(e.target.value)} /> + ), + CommandGroup: ({ children }: any) =>
{children}
, + CommandItem: ({ children, onSelect }: any) => ( +
+ {children} +
+ ), + CommandList: ({ children }: any) =>
{children}
, + CommandEmpty: ({ children }: any) =>
{children}
, Select: ({ children, value, onValueChange }: any) => (
@@ -181,13 +193,63 @@ vi.mock("@src/components/mcp/McpView", () => ({ default: () => null, })) -// Mock Tab components -vi.mock("../common/Tab", () => ({ +vi.mock("../../common/Tab", () => ({ Tab: ({ children }: any) =>
{children}
, - TabContent: React.forwardRef(({ children }, ref) =>
{children}
), + TabContent: React.forwardRef(({ children, ...props }, ref) => ( +
+ {children} +
+ )), TabHeader: ({ children }: any) =>
{children}
, - TabList: ({ children }: any) =>
{children}
, - TabTrigger: React.forwardRef(({ children }, ref) => ), + TabList: ({ children, value, onValueChange }: any) => ( +
+ {React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child + } + + const element = child as React.ReactElement + return React.cloneElement(element, { + isSelected: element.props.value === value, + onSelect: () => onValueChange(element.props.value), + }) + })} +
+ ), + TabTrigger: React.forwardRef(({ children, onSelect, ...props }, ref) => ( + + )), +})) +vi.mock("@src/components/common/Tab", () => ({ + Tab: ({ children }: any) =>
{children}
, + TabContent: React.forwardRef(({ children, ...props }, ref) => ( +
+ {children} +
+ )), + TabHeader: ({ children }: any) =>
{children}
, + TabList: ({ children, value, onValueChange }: any) => ( +
+ {React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child + } + + const element = child as React.ReactElement + return React.cloneElement(element, { + isSelected: element.props.value === value, + onSelect: () => onValueChange(element.props.value), + }) + })} +
+ ), + TabTrigger: React.forwardRef(({ children, onSelect, ...props }, ref) => ( + + )), })) // Mock all child components to isolate the test @@ -195,25 +257,30 @@ vi.mock("../ApiConfigManager", () => ({ default: () => null, })) +const mockApiOptions = ({ apiConfiguration, setApiConfigurationField }: any) => ( +
+ {apiConfiguration.apiProvider} + setApiConfigurationField("basetenApiKey", event.target.value)} + /> + {["openrouter", "baseten", "deepseek", "friendli"].map((provider) => ( + + ))} +
+) + vi.mock("../ApiOptions", () => ({ - default: ({ apiConfiguration, setApiConfigurationField }: any) => ( -
- {apiConfiguration.apiProvider} - setApiConfigurationField("basetenApiKey", event.target.value)} - /> - {["openrouter", "baseten", "deepseek", "friendli"].map((provider) => ( - - ))} -
- ), + default: mockApiOptions, +})) +vi.mock("@src/components/settings/ApiOptions", () => ({ + default: mockApiOptions, })) vi.mock("../AutoApproveSettings", () => ({ @@ -228,6 +295,43 @@ vi.mock("../Section", () => ({ Section: ({ children }: any) =>
{children}
, })) +vi.mock("../SearchableSetting", () => ({ + SearchableSetting: ({ children }: any) =>
{children}
, +})) +vi.mock("../useSettingsSearch", () => ({ + SearchIndexProvider: ({ children }: any) => <>{children}, + useSearchIndexRegistry: () => ({ + contextValue: { registerSetting: vi.fn() }, + index: [], + }), + useSettingsSearch: () => ({ + searchQuery: "", + setSearchQuery: vi.fn(), + results: [], + isOpen: false, + setIsOpen: vi.fn(), + clearSearch: vi.fn(), + }), +})) +vi.mock("@src/components/settings/SearchableSetting", () => ({ + SearchableSetting: ({ children }: any) =>
{children}
, +})) +vi.mock("@src/components/settings/useSettingsSearch", () => ({ + SearchIndexProvider: ({ children }: any) => <>{children}, + useSearchIndexRegistry: () => ({ + contextValue: { registerSetting: vi.fn() }, + index: [], + }), + useSettingsSearch: () => ({ + searchQuery: "", + setSearchQuery: vi.fn(), + results: [], + isOpen: false, + setIsOpen: vi.fn(), + clearSearch: vi.fn(), + }), +})) + // Mock all settings components vi.mock("../CheckpointSettings", () => ({ CheckpointSettings: () => null, @@ -263,6 +367,11 @@ vi.mock("../UISettings", () => ({ vi.mock("../SettingsSearch", () => ({ SettingsSearch: () => null, })) +vi.mock("@src/components/settings/SettingsSearch", () => ({ + SettingsSearch: () => null, +})) + +let SettingsView: typeof import("../SettingsView").default describe("SettingsView - Change Detection Fix", () => { let queryClient: QueryClient @@ -332,9 +441,16 @@ describe("SettingsView - Change Detection Fix", () => { autoCloseZooOpenedFiles: true, autoCloseZooOpenedFilesAfterUserEdited: false, autoCloseZooOpenedNewFiles: false, + mode: "code", ...overrides, }) + beforeAll(async () => { + // Import after mocks are registered so the isolated tests use the + // lightweight child component mocks above instead of the full settings UI. + SettingsView = (await import("../SettingsView")).default + }) + beforeEach(() => { vi.clearAllMocks() queryClient = new QueryClient({ @@ -373,7 +489,7 @@ describe("SettingsView - Change Detection Fix", () => { // onDone should be called expect(onDone).toHaveBeenCalled() - }) + }, 10000) // These tests are passing for the basic case but failing due to vi.doMock limitations // The core fix has been verified - when no actual changes are made, no unsaved changes dialog appears @@ -394,7 +510,7 @@ describe("SettingsView - Change Detection Fix", () => { // - null -> value (initialization from null) expect(true).toBe(true) // Placeholder - the real test is the running system - }) + }, 10000) it("preserves a DeepSeek provider edit after saving Baseten when the same import timestamp replays", async () => { const onDone = vi.fn() @@ -473,7 +589,7 @@ describe("SettingsView - Change Detection Fix", () => { apiProvider: "deepseek", }), }) - }) + }, 10000) it("resets cached provider state when a new import timestamp arrives", async () => { const onDone = vi.fn() @@ -525,5 +641,152 @@ describe("SettingsView - Change Detection Fix", () => { await waitFor(() => { expect(screen.getByTestId("save-button")).toBeDisabled() }) + }, 10000) + + describe("mode synchronization", () => { + it("resets changeDetected and syncs cachedState when mode changes after dirty state", async () => { + const onDone = vi.fn() + let extensionState = createExtensionState({ + mode: "code", + apiConfiguration: { + apiProvider: "openai", + apiModelId: "gpt-4.1", + }, + }) + + ;(useExtensionState as any).mockImplementation(() => extensionState) + + const { rerender } = render( + + + , + ) + + await waitFor(() => { + expect(screen.getByTestId("provider-value")).toHaveTextContent("openai") + }) + + // Make a dirty change by switching provider + fireEvent.click(screen.getByTestId("set-provider-baseten")) + expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten") + + // Verify save button is enabled (dirty state) + const saveButton = screen.getByTestId("save-button") as HTMLButtonElement + expect(saveButton.disabled).toBe(false) + + // Now change only the mode-dependent values while keeping extensionState's + // object identity stable. This makes the `mode` dependency load-bearing: + // without it, React would not re-run the sync effect. + await act(async () => { + extensionState.mode = "ask" + extensionState.apiConfiguration = { + apiProvider: "openrouter", + apiModelId: "claude-3.5-sonnet", + } + + rerender( + + + , + ) + }) + + // Let the mode sync effect run + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + // Verify cachedState reflects the new mode's settings + await waitFor(() => { + expect(screen.getByTestId("provider-value")).toHaveTextContent("openrouter") + }) + + // Verify changeDetected is reset (save button should be disabled) + const updatedSaveButton = screen.getByTestId("save-button") as HTMLButtonElement + expect(updatedSaveButton.disabled).toBe(true) + + // Make another dirty change while already in the new mode. + fireEvent.click(screen.getByTestId("set-provider-deepseek")) + expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek") + expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(false) + + // Re-render with a new extensionState identity but the same mode and config + // name. If prevMode.current is not updated during the first mode transition, + // the stale ref makes this same-mode render look like another mode change and + // incorrectly overwrites the dirty cached provider below. + await act(async () => { + extensionState = createExtensionState({ + mode: "ask", + apiConfiguration: { + apiProvider: "friendli", + apiModelId: "friendli-model", + }, + }) + ;(useExtensionState as any).mockImplementation(() => extensionState) + + rerender( + + + , + ) + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek") + expect((screen.getByTestId("save-button") as HTMLButtonElement).disabled).toBe(false) + }, 20000) + + it("does not trigger sync when mode has not changed", async () => { + const onDone = vi.fn() + let extensionState = createExtensionState({ + mode: "code", + apiConfiguration: { + apiProvider: "openai", + apiModelId: "gpt-4.1", + }, + }) + + ;(useExtensionState as any).mockImplementation(() => extensionState) + + const { rerender } = render( + + + , + ) + + await waitFor(() => { + expect(screen.getByTestId("provider-value")).toHaveTextContent("openai") + }) + + // Make a dirty change so we can verify it isn't overwritten by a sync + fireEvent.click(screen.getByTestId("set-provider-baseten")) + expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten") + + // Re-render with a new extensionState identity but the same mode and config + // name. This makes the guard load-bearing because the effect is eligible to + // re-run from the extensionState dependency, but must not sync cachedState. + await act(async () => { + extensionState = createExtensionState({ + mode: "code", + apiConfiguration: { + apiProvider: "openai", + apiModelId: "gpt-4.1", + }, + }) + ;(useExtensionState as any).mockImplementation(() => extensionState) + + rerender( + + + , + ) + }) + + // Provider value should remain unchanged from the dirty state + expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten") + }, 20000) }) }) From c6279d8476813ec6f2e87a46e303cf7135b9d341 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Wed, 22 Jul 2026 04:30:03 +0300 Subject: [PATCH 45/89] Fix codebase indexing for plain text files (#938) * fix(code-index): index plain text files * fix(tree-sitter): skip plain text definition parsing * fix(code-index): address plain text review feedback * fix(tree-sitter): preserve Scala structural parsing * test(code-index): remove redundant async usage --- .../processors/__tests__/parser.spec.ts | 25 ++++++++ .../processors/__tests__/parser.txt.spec.ts | 58 +++++++++++++++++++ src/services/code-index/processors/parser.ts | 2 +- .../code-index/shared/supported-extensions.ts | 11 ++-- src/services/shared/fallback-extensions.ts | 29 ++++++++++ .../__tests__/plainTextIntegration.spec.ts | 37 ++++++++++++ src/services/tree-sitter/index.ts | 8 +++ 7 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/services/code-index/processors/__tests__/parser.txt.spec.ts create mode 100644 src/services/shared/fallback-extensions.ts create mode 100644 src/services/tree-sitter/__tests__/plainTextIntegration.spec.ts diff --git a/src/services/code-index/processors/__tests__/parser.spec.ts b/src/services/code-index/processors/__tests__/parser.spec.ts index 68b523c7df..1c8154e03f 100644 --- a/src/services/code-index/processors/__tests__/parser.spec.ts +++ b/src/services/code-index/processors/__tests__/parser.spec.ts @@ -267,6 +267,31 @@ describe("CodeParser", () => { }) describe("_chunkTextByLines", () => { + it("should not emit a chunk whose segment hash has already been seen", () => { + const lines = ["Fallback content long enough to produce a chunk without being filtered out."] + const seenSegmentHashes = new Set() + + const firstResult = parser["_chunkTextByLines"]( + lines, + "manual.txt", + "hash", + "fallback_chunk", + seenSegmentHashes, + ) + const duplicateResult = parser["_chunkTextByLines"]( + lines, + "manual.txt", + "hash", + "fallback_chunk", + seenSegmentHashes, + ) + + expect(firstResult).toHaveLength(1) + expect(firstResult[0].segmentHash).toMatch(/^[a-f0-9]{64}$/) + expect(seenSegmentHashes).toEqual(new Set([firstResult[0].segmentHash])) + expect(duplicateResult).toEqual([]) + }) + it("should handle oversized lines by splitting them", async () => { const longLine = "a".repeat(2000) const lines = ["normal", longLine, "normal"] diff --git a/src/services/code-index/processors/__tests__/parser.txt.spec.ts b/src/services/code-index/processors/__tests__/parser.txt.spec.ts new file mode 100644 index 0000000000..0fa9526916 --- /dev/null +++ b/src/services/code-index/processors/__tests__/parser.txt.spec.ts @@ -0,0 +1,58 @@ +import { CodeParser } from "../parser" +import { scannerExtensions, shouldUseFallbackChunking } from "../../shared/supported-extensions" + +vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({ + TelemetryService: { + instance: { + captureEvent: vi.fn(), + }, + }, +})) + +describe("CodeParser - plain text support", () => { + it("supports .txt files through fallback chunking", async () => { + expect(scannerExtensions).toContain(".txt") + expect(shouldUseFallbackChunking(".txt")).toBe(true) + + const content = [ + "Zoo Code plain text indexing regression test.", + "This sentence contains searchable content that only exists in the text file.", + "The fallback parser should preserve every line while creating an indexable chunk.", + ].join("\n") + + const blocks = await new CodeParser().parseFile("manual.txt", { + content, + fileHash: "txt-file-hash", + }) + + expect(blocks).toHaveLength(1) + expect(blocks[0]).toMatchObject({ + file_path: "manual.txt", + type: "fallback_chunk", + start_line: 1, + end_line: 3, + content, + fileHash: "txt-file-hash", + segmentHash: expect.any(String), + }) + }) + + it("parses uppercase .TXT extensions", async () => { + expect(shouldUseFallbackChunking(".TXT")).toBe(true) + + const content = "Uppercase plain-text extension content long enough to produce a fallback chunk." + const blocks = await new CodeParser().parseFile("manual.TXT", { + content, + fileHash: "uppercase-txt-file-hash", + }) + + expect(blocks).toHaveLength(1) + expect(blocks[0]).toMatchObject({ + file_path: "manual.TXT", + type: "fallback_chunk", + content, + fileHash: "uppercase-txt-file-hash", + segmentHash: expect.any(String), + }) + }) +}) diff --git a/src/services/code-index/processors/parser.ts b/src/services/code-index/processors/parser.ts index 8611884ade..8445ea1971 100644 --- a/src/services/code-index/processors/parser.ts +++ b/src/services/code-index/processors/parser.ts @@ -45,7 +45,7 @@ export class CodeParser implements ICodeParser { let content: string let fileHash: string - if (options?.content) { + if (options?.content !== undefined) { content = options.content fileHash = options.fileHash || this.createFileHash(content) } else { diff --git a/src/services/code-index/shared/supported-extensions.ts b/src/services/code-index/shared/supported-extensions.ts index 80dd7102ff..a5f93e161f 100644 --- a/src/services/code-index/shared/supported-extensions.ts +++ b/src/services/code-index/shared/supported-extensions.ts @@ -1,4 +1,7 @@ import { extensions as allExtensions } from "../../tree-sitter" +import { fallbackExtensions, isFallbackExtension } from "../../shared/fallback-extensions" + +export { fallbackExtensions } // Include all extensions including markdown for the scanner export const scannerExtensions = allExtensions @@ -18,17 +21,11 @@ export const scannerExtensions = allExtensions * * Note: Do NOT remove parser cases from languageParser.ts as they may be used elsewhere */ -export const fallbackExtensions = [ - ".vb", // Visual Basic .NET - no dedicated WASM parser - ".scala", // Scala - uses fallback chunking instead of Lua query workaround - ".swift", // Swift - uses fallback chunking due to parser instability -] - /** * Check if a file extension should use fallback chunking * @param extension File extension (including the dot) * @returns true if the extension should use fallback chunking */ export function shouldUseFallbackChunking(extension: string): boolean { - return fallbackExtensions.includes(extension.toLowerCase()) + return isFallbackExtension(extension) } diff --git a/src/services/shared/fallback-extensions.ts b/src/services/shared/fallback-extensions.ts new file mode 100644 index 0000000000..ee78b61b0f --- /dev/null +++ b/src/services/shared/fallback-extensions.ts @@ -0,0 +1,29 @@ +/** + * Extensions that should not be parsed for structural definitions and should + * instead use line-based fallback chunking where indexing is supported. + */ +export const fallbackExtensions = [".txt", ".vb", ".scala", ".swift"] as const + +/** + * Fallback extensions that do not have a structural parser. Scala and Swift + * still support structural parsing outside code indexing. + */ +export const nonStructuralExtensions = [".txt", ".vb"] as const + +/** + * Check whether a file extension should bypass structural parsing. + * + * @param extension File extension, including the leading dot + */ +export function isFallbackExtension(extension: string): boolean { + return (fallbackExtensions as readonly string[]).includes(extension.toLowerCase()) +} + +/** + * Check whether a file extension should bypass structural parsing entirely. + * + * @param extension File extension, including the leading dot + */ +export function isNonStructuralExtension(extension: string): boolean { + return (nonStructuralExtensions as readonly string[]).includes(extension.toLowerCase()) +} diff --git a/src/services/tree-sitter/__tests__/plainTextIntegration.spec.ts b/src/services/tree-sitter/__tests__/plainTextIntegration.spec.ts new file mode 100644 index 0000000000..1635a40d04 --- /dev/null +++ b/src/services/tree-sitter/__tests__/plainTextIntegration.spec.ts @@ -0,0 +1,37 @@ +// Mocks must come first, before imports + +vi.mock("fs/promises", () => ({ + readFile: vi.fn(), +})) + +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn().mockResolvedValue(true), +})) + +vi.mock("../languageParser", () => ({ + loadRequiredLanguageParsers: vi.fn(), +})) + +import * as fs from "fs/promises" +import { loadRequiredLanguageParsers } from "../languageParser" +import { parseSourceCodeDefinitionsForFile } from "../index" + +describe("Non-structural Extension Integration Tests", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it.each(["manual.txt", "legacy.vb"])( + "returns undefined for %s without loading a tree-sitter parser", + async (filePath) => { + const result = await parseSourceCodeDefinitionsForFile(filePath) + + expect(result).toBeUndefined() + }, + ) + + afterEach(() => { + expect(loadRequiredLanguageParsers).not.toHaveBeenCalled() + expect(fs.readFile).not.toHaveBeenCalled() + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 3c124e5b74..da20a07de7 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -5,6 +5,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { parseMarkdown } from "./markdownParser" import { RooIgnoreController } from "../../core/ignore/RooIgnoreController" import { QueryCapture } from "web-tree-sitter" +import { isNonStructuralExtension } from "../shared/fallback-extensions" // Private constant const DEFAULT_MIN_COMPONENT_LINES_VALUE = 4 @@ -66,6 +67,8 @@ const extensions = [ // Markdown "md", "markdown", + // Plain text + "txt", // JSON "json", // CSS @@ -111,6 +114,11 @@ export async function parseSourceCodeDefinitionsForFile( return undefined } + // Files without a structural parser have no definitions to extract + if (isNonStructuralExtension(ext)) { + return undefined + } + // Special case for markdown files if (ext === ".md" || ext === ".markdown") { // Check if we have permission to access this file From a7d79689fe5be12417a04b3eeef66406d40cd4e4 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Wed, 22 Jul 2026 04:34:03 +0300 Subject: [PATCH 46/89] fix(mcp): remove obsolete server creation translations (#943) * fix(mcp): remove obsolete server creation translations * test(mcp): remove stale server creation references * test(mcp): fix no-server prompt snapshot --- ...mcp-server-creation-disabled.snap => no-mcp-servers.snap} | 0 src/core/prompts/__tests__/add-custom-instructions.spec.ts | 5 ++--- webview-ui/src/i18n/locales/ca/mcp.json | 5 ----- webview-ui/src/i18n/locales/de/mcp.json | 5 ----- webview-ui/src/i18n/locales/en/mcp.json | 5 ----- webview-ui/src/i18n/locales/es/mcp.json | 5 ----- webview-ui/src/i18n/locales/fr/mcp.json | 5 ----- webview-ui/src/i18n/locales/hi/mcp.json | 5 ----- webview-ui/src/i18n/locales/id/mcp.json | 5 ----- webview-ui/src/i18n/locales/it/mcp.json | 5 ----- webview-ui/src/i18n/locales/ja/mcp.json | 5 ----- webview-ui/src/i18n/locales/ko/mcp.json | 5 ----- webview-ui/src/i18n/locales/nl/mcp.json | 5 ----- webview-ui/src/i18n/locales/pl/mcp.json | 5 ----- webview-ui/src/i18n/locales/pt-BR/mcp.json | 5 ----- webview-ui/src/i18n/locales/ru/mcp.json | 5 ----- webview-ui/src/i18n/locales/tr/mcp.json | 5 ----- webview-ui/src/i18n/locales/vi/mcp.json | 5 ----- webview-ui/src/i18n/locales/zh-CN/mcp.json | 5 ----- webview-ui/src/i18n/locales/zh-TW/mcp.json | 5 ----- 20 files changed, 2 insertions(+), 93 deletions(-) rename src/core/prompts/__tests__/__snapshots__/add-custom-instructions/{mcp-server-creation-disabled.snap => no-mcp-servers.snap} (100%) diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap similarity index 100% rename from src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap rename to src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap diff --git a/src/core/prompts/__tests__/add-custom-instructions.spec.ts b/src/core/prompts/__tests__/add-custom-instructions.spec.ts index 00d32c2c29..5c9d7edf2c 100644 --- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts +++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts @@ -236,7 +236,7 @@ describe("addCustomInstructions", () => { expect(prompt).toMatchFileSnapshot("./__snapshots__/add-custom-instructions/ask-mode-prompt.snap") }) - it("should exclude MCP server creation info when disabled", async () => { + it("should generate the prompt when the MCP hub has no servers", async () => { const mockMcpHub = createMockMcpHub(false) const prompt = await SYSTEM_PROMPT( @@ -255,8 +255,7 @@ describe("addCustomInstructions", () => { undefined, // partialReadsEnabled ) - expect(prompt).not.toContain("Creating an MCP Server") - expect(prompt).toMatchFileSnapshot("./__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap") + expect(prompt).toMatchFileSnapshot("./__snapshots__/add-custom-instructions/no-mcp-servers.snap") }) it("should prioritize mode-specific rules for code mode", async () => { diff --git a/webview-ui/src/i18n/locales/ca/mcp.json b/webview-ui/src/i18n/locales/ca/mcp.json index 0bda1f751a..6e9e958a58 100644 --- a/webview-ui/src/i18n/locales/ca/mcp.json +++ b/webview-ui/src/i18n/locales/ca/mcp.json @@ -8,11 +8,6 @@ "title": "Activa els servidors MCP", "description": "Activa-ho perquè Zoo pugui utilitzar eines dels servidors MCP connectats. Això dóna més capacitats a Zoo. Si no vols utilitzar aquestes eines addicionals, desactiva-ho per ajudar a reduir el cost dels tokens API." }, - "enableServerCreation": { - "title": "Activa la creació de servidors MCP", - "description": "Activa-ho perquè Zoo t'ajudi a crear <1>nous servidors MCP personalitzats. <0>Més informació sobre la creació de servidors", - "hint": "Consell: Per reduir el cost dels tokens API, desactiva aquesta opció quan no demanis a Zoo que creï un nou servidor MCP." - }, "editGlobalMCP": "Edita MCP global", "editProjectMCP": "Edita MCP del projecte", "learnMoreEditingSettings": "Més informació sobre com editar fitxers de configuració MCP", diff --git a/webview-ui/src/i18n/locales/de/mcp.json b/webview-ui/src/i18n/locales/de/mcp.json index a8aac6ee37..c8872d4de3 100644 --- a/webview-ui/src/i18n/locales/de/mcp.json +++ b/webview-ui/src/i18n/locales/de/mcp.json @@ -8,11 +8,6 @@ "title": "MCP-Server aktivieren", "description": "Schalte dies EIN, damit Zoo Tools von verbundenen MCP-Servern verwenden kann. Dies gibt Zoo mehr Möglichkeiten. Wenn du diese zusätzlichen Tools nicht verwenden möchtest, schalte es AUS, um API-Token-Kosten zu senken." }, - "enableServerCreation": { - "title": "MCP-Server-Erstellung aktivieren", - "description": "Aktiviere dies, damit Zoo dir helfen kann, <1>neue benutzerdefinierte MCP-Server zu erstellen. <0>Erfahre mehr über Server-Erstellung", - "hint": "Hinweis: Um API-Token-Kosten zu senken, deaktiviere diese Einstellung, wenn du Zoo nicht aktiv darum bittest, einen neuen MCP-Server zu erstellen." - }, "editGlobalMCP": "Globale MCP bearbeiten", "editProjectMCP": "Projekt-MCP bearbeiten", "learnMoreEditingSettings": "Mehr über das Bearbeiten von MCP-Einstellungsdateien erfahren", diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 9256146311..f0c033fe74 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -8,11 +8,6 @@ "title": "Enable MCP Servers", "description": "Turn this ON to let Zoo use tools from connected MCP servers. This gives Zoo more capabilities. If you don't plan to use these extra tools, turn it OFF to help reduce API token costs." }, - "enableServerCreation": { - "title": "Enable MCP Server Creation", - "description": "Enable this to have Zoo help you build <1>new custom MCP servers. <0>Learn about server creation", - "hint": "Hint: To reduce API token costs, disable this setting when you are not actively asking Zoo to create a new MCP server." - }, "editGlobalMCP": "Edit Global MCP", "editProjectMCP": "Edit Project MCP", "refreshMCP": "Refresh MCP Servers", diff --git a/webview-ui/src/i18n/locales/es/mcp.json b/webview-ui/src/i18n/locales/es/mcp.json index 768b4b6e59..e31f70c5ce 100644 --- a/webview-ui/src/i18n/locales/es/mcp.json +++ b/webview-ui/src/i18n/locales/es/mcp.json @@ -8,11 +8,6 @@ "title": "Activar servidores MCP", "description": "Actívalo para que Zoo pueda usar herramientas de servidores MCP conectados. Esto le da más capacidades a Zoo. Si no planeas usar estas herramientas extra, desactívalo para ayudar a reducir los costes de tokens API." }, - "enableServerCreation": { - "title": "Activar creación de servidores MCP", - "description": "Actívalo para que Zoo te ayude a crear <1>nuevos servidores MCP personalizados. <0>Más información sobre la creación de servidores", - "hint": "Consejo: Para reducir los costes de tokens API, desactiva esta opción cuando no le pidas a Zoo que cree un nuevo servidor MCP." - }, "editGlobalMCP": "Editar MCP global", "editProjectMCP": "Editar MCP del proyecto", "learnMoreEditingSettings": "Más información sobre cómo editar archivos de configuración MCP", diff --git a/webview-ui/src/i18n/locales/fr/mcp.json b/webview-ui/src/i18n/locales/fr/mcp.json index 1698a7f241..f1f0032680 100644 --- a/webview-ui/src/i18n/locales/fr/mcp.json +++ b/webview-ui/src/i18n/locales/fr/mcp.json @@ -8,11 +8,6 @@ "title": "Activer les serveurs MCP", "description": "Active cette option pour que Zoo puisse utiliser des outils provenant de serveurs MCP connectés. Cela donne plus de capacités à Zoo. Si tu ne comptes pas utiliser ces outils supplémentaires, désactive-la pour réduire les coûts de tokens API." }, - "enableServerCreation": { - "title": "Activer la création de serveurs MCP", - "description": "Active cette option pour que Zoo t'aide à créer de <1>nouveaux serveurs MCP personnalisés. <0>En savoir plus sur la création de serveurs", - "hint": "Astuce : Pour réduire les coûts de tokens API, désactive cette option quand tu ne demandes pas à Zoo de créer un nouveau serveur MCP." - }, "editGlobalMCP": "Modifier le MCP global", "editProjectMCP": "Modifier le MCP du projet", "learnMoreEditingSettings": "En savoir plus sur la modification des fichiers de configuration MCP", diff --git a/webview-ui/src/i18n/locales/hi/mcp.json b/webview-ui/src/i18n/locales/hi/mcp.json index cc2290f9d7..7dd8b8a415 100644 --- a/webview-ui/src/i18n/locales/hi/mcp.json +++ b/webview-ui/src/i18n/locales/hi/mcp.json @@ -8,11 +8,6 @@ "title": "MCP सर्वर सक्षम करें", "description": "इसे ON करो ताकि Zoo जुड़े हुए MCP सर्वरों से टूल्स इस्तेमाल कर सके। इससे Zoo को और क्षमताएँ मिलती हैं। अगर तुम ये अतिरिक्त टूल्स इस्तेमाल नहीं करना चाहते, तो इसे OFF करो ताकि API टोकन लागत कम हो सके।" }, - "enableServerCreation": { - "title": "MCP सर्वर बनाना सक्षम करें", - "description": "इसे ON करो ताकि Zoo तुम्हारी मदद से <1>नए कस्टम MCP सर्वर बना सके। <0>सर्वर बनाना जानें", - "hint": "टिप: API टोकन लागत कम करने के लिए, जब Zoo से नया MCP सर्वर नहीं बनवा रहे हो तो इस सेटिंग को बंद कर दो।" - }, "editGlobalMCP": "ग्लोबल MCP एडिट करें", "editProjectMCP": "प्रोजेक्ट MCP एडिट करें", "learnMoreEditingSettings": "MCP सेटिंग्स फाइल एडिट करने के बारे में जानें", diff --git a/webview-ui/src/i18n/locales/id/mcp.json b/webview-ui/src/i18n/locales/id/mcp.json index 368da63b93..e22b10924a 100644 --- a/webview-ui/src/i18n/locales/id/mcp.json +++ b/webview-ui/src/i18n/locales/id/mcp.json @@ -8,11 +8,6 @@ "title": "Aktifkan Server MCP", "description": "Nyalakan ini untuk membiarkan Zoo menggunakan tools dari server MCP yang terhubung. Ini memberikan Zoo lebih banyak kemampuan. Jika Anda tidak berencana menggunakan tools tambahan ini, matikan untuk membantu mengurangi biaya token API." }, - "enableServerCreation": { - "title": "Aktifkan Pembuatan Server MCP", - "description": "Aktifkan ini agar Zoo membantu Anda membangun server MCP kustom <1>baru. <0>Pelajari tentang pembuatan server", - "hint": "Petunjuk: Untuk mengurangi biaya token API, nonaktifkan pengaturan ini ketika Anda tidak secara aktif meminta Zoo untuk membuat server MCP baru." - }, "editGlobalMCP": "Edit MCP Global", "editProjectMCP": "Edit MCP Proyek", "refreshMCP": "Refresh Server MCP", diff --git a/webview-ui/src/i18n/locales/it/mcp.json b/webview-ui/src/i18n/locales/it/mcp.json index e5f4aff9eb..dc8e6caa6c 100644 --- a/webview-ui/src/i18n/locales/it/mcp.json +++ b/webview-ui/src/i18n/locales/it/mcp.json @@ -8,11 +8,6 @@ "title": "Abilita server MCP", "description": "Attiva questa opzione per permettere a Zoo di usare strumenti dai server MCP collegati. Questo dà a Zoo più capacità. Se non vuoi usare questi strumenti extra, disattiva per ridurre i costi dei token API." }, - "enableServerCreation": { - "title": "Abilita creazione server MCP", - "description": "Abilita questa opzione per farti aiutare da Zoo a creare <1>nuovi server MCP personalizzati. <0>Scopri di più sulla creazione di server", - "hint": "Suggerimento: Per ridurre i costi dei token API, disattiva questa impostazione quando non chiedi a Zoo di creare un nuovo server MCP." - }, "editGlobalMCP": "Modifica MCP globale", "editProjectMCP": "Modifica MCP del progetto", "learnMoreEditingSettings": "Scopri di più sulla modifica dei file di configurazione MCP", diff --git a/webview-ui/src/i18n/locales/ja/mcp.json b/webview-ui/src/i18n/locales/ja/mcp.json index d415e30f08..66151f3d60 100644 --- a/webview-ui/src/i18n/locales/ja/mcp.json +++ b/webview-ui/src/i18n/locales/ja/mcp.json @@ -8,11 +8,6 @@ "title": "MCPサーバーを有効化", "description": "これをONにすると、Rooが接続されたMCPサーバーのツールを使えるようになるよ。Rooの機能が増える!追加ツールを使わないなら、APIトークンのコストを抑えるためにOFFにしてね。" }, - "enableServerCreation": { - "title": "MCPサーバー作成を有効化", - "description": "これをONにすると、Rooが<1>新しいカスタムMCPサーバーを作るのを手伝ってくれるよ。<0>サーバー作成について詳しく", - "hint": "ヒント: APIトークンのコストを抑えたいときは、Rooに新しいMCPサーバーを作らせないときにこの設定をOFFにしてね。" - }, "editGlobalMCP": "グローバルMCPを編集", "editProjectMCP": "プロジェクトMCPを編集", "learnMoreEditingSettings": "MCP設定ファイルの編集方法を詳しく見る", diff --git a/webview-ui/src/i18n/locales/ko/mcp.json b/webview-ui/src/i18n/locales/ko/mcp.json index b0ecb215e1..2327ef0ca5 100644 --- a/webview-ui/src/i18n/locales/ko/mcp.json +++ b/webview-ui/src/i18n/locales/ko/mcp.json @@ -8,11 +8,6 @@ "title": "MCP 서버 활성화", "description": "이걸 켜면 Roo가 연결된 MCP 서버의 도구를 쓸 수 있어. Roo의 능력이 더 늘어나! 추가 도구를 쓸 생각이 없다면, API 토큰 비용을 줄이기 위해 꺼 두는 게 좋아." }, - "enableServerCreation": { - "title": "MCP 서버 생성 활성화", - "description": "이걸 켜면 Roo가 <1>새로운 맞춤형 MCP 서버를 만드는 걸 도와줄 수 있어. <0>서버 생성에 대해 알아보기", - "hint": "팁: API 토큰 비용을 줄이고 싶으면, Roo에게 새 MCP 서버를 만들라고 하지 않을 때 이 설정을 꺼 둬." - }, "editGlobalMCP": "글로벌 MCP 편집", "editProjectMCP": "프로젝트 MCP 편집", "learnMoreEditingSettings": "MCP 설정 파일 편집 방법 더 알아보기", diff --git a/webview-ui/src/i18n/locales/nl/mcp.json b/webview-ui/src/i18n/locales/nl/mcp.json index 8a4b2089c0..035f4fa869 100644 --- a/webview-ui/src/i18n/locales/nl/mcp.json +++ b/webview-ui/src/i18n/locales/nl/mcp.json @@ -8,11 +8,6 @@ "title": "MCP-servers inschakelen", "description": "Indien ingeschakeld, kan Zoo communiceren met MCP-servers voor geavanceerde functionaliteit. Gebruik je geen MCP, dan kun je dit uitschakelen om het tokengebruik te verminderen." }, - "enableServerCreation": { - "title": "Aanmaken van MCP-server inschakelen", - "description": "Indien ingeschakeld, kan Zoo je helpen nieuwe MCP-servers te maken via commando's zoals 'voeg een nieuwe tool toe aan...'. Heb je dit niet nodig, schakel het dan uit om het tokengebruik te verminderen.", - "hint": "Tip: Zet deze instelling uit als je Zoo niet actief vraagt om een nieuwe MCP-server te maken, om API-tokenkosten te besparen." - }, "editGlobalMCP": "Globale MCP bewerken", "editProjectMCP": "Project-MCP bewerken", "learnMoreEditingSettings": "Meer over het bewerken van MCP-instellingen", diff --git a/webview-ui/src/i18n/locales/pl/mcp.json b/webview-ui/src/i18n/locales/pl/mcp.json index 5d7ab75550..4854910d5d 100644 --- a/webview-ui/src/i18n/locales/pl/mcp.json +++ b/webview-ui/src/i18n/locales/pl/mcp.json @@ -8,11 +8,6 @@ "title": "Włącz serwery MCP", "description": "Włącz to, aby Zoo mógł korzystać z narzędzi połączonych serwerów MCP. Daje to Zoo więcej możliwości. Jeśli nie planujesz korzystać z tych dodatkowych narzędzi, wyłącz to, aby zmniejszyć koszty tokenów API." }, - "enableServerCreation": { - "title": "Włącz tworzenie serwerów MCP", - "description": "Włącz to, aby Zoo mógł pomóc ci tworzyć <1>nowe niestandardowe serwery MCP. <0>Dowiedz się więcej o tworzeniu serwerów", - "hint": "Wskazówka: Aby zmniejszyć koszty tokenów API, wyłącz tę opcję, gdy nie prosisz Zoo o utworzenie nowego serwera MCP." - }, "editGlobalMCP": "Edytuj globalny MCP", "editProjectMCP": "Edytuj MCP projektu", "learnMoreEditingSettings": "Dowiedz się więcej o edycji plików ustawień MCP", diff --git a/webview-ui/src/i18n/locales/pt-BR/mcp.json b/webview-ui/src/i18n/locales/pt-BR/mcp.json index 70b48d0130..a6acac9283 100644 --- a/webview-ui/src/i18n/locales/pt-BR/mcp.json +++ b/webview-ui/src/i18n/locales/pt-BR/mcp.json @@ -8,11 +8,6 @@ "title": "Ativar servidores MCP", "description": "Ative para que o Zoo possa usar ferramentas de servidores MCP conectados. Isso dá mais capacidades ao Zoo. Se você não pretende usar essas ferramentas extras, desative para ajudar a reduzir os custos de tokens da API." }, - "enableServerCreation": { - "title": "Ativar criação de servidores MCP", - "description": "Ative para que o Zoo possa te ajudar a criar <1>novos servidores MCP personalizados. <0>Saiba mais sobre criação de servidores", - "hint": "Dica: Para reduzir os custos de tokens da API, desative esta configuração quando não estiver pedindo ao Zoo para criar um novo servidor MCP." - }, "editGlobalMCP": "Editar MCP global", "editProjectMCP": "Editar MCP do projeto", "learnMoreEditingSettings": "Saiba mais sobre como editar arquivos de configuração MCP", diff --git a/webview-ui/src/i18n/locales/ru/mcp.json b/webview-ui/src/i18n/locales/ru/mcp.json index e67e063f4c..0322016bf4 100644 --- a/webview-ui/src/i18n/locales/ru/mcp.json +++ b/webview-ui/src/i18n/locales/ru/mcp.json @@ -8,11 +8,6 @@ "title": "Включить серверы MCP", "description": "Включи, чтобы Zoo мог использовать инструменты с подключённых серверов MCP. Это даст Zoo больше возможностей. Если не планируешь использовать эти дополнительные инструменты, выключи для экономии токенов API." }, - "enableServerCreation": { - "title": "Включить создание серверов MCP", - "description": "Включи, чтобы Zoo помогал создавать <1>новые кастомные серверы MCP. <0>Подробнее о создании серверов", - "hint": "Совет: чтобы снизить расходы на токены API, отключай эту настройку, когда не просишь Zoo создать новый сервер MCP." - }, "editGlobalMCP": "Редактировать глобальный MCP", "editProjectMCP": "Редактировать проектный MCP", "learnMoreEditingSettings": "Подробнее о редактировании файлов настроек MCP", diff --git a/webview-ui/src/i18n/locales/tr/mcp.json b/webview-ui/src/i18n/locales/tr/mcp.json index 7aab5b8a4e..691e30c1dc 100644 --- a/webview-ui/src/i18n/locales/tr/mcp.json +++ b/webview-ui/src/i18n/locales/tr/mcp.json @@ -8,11 +8,6 @@ "title": "MCP Sunucularını Etkinleştir", "description": "Bunu AÇ, böylece Zoo bağlı MCP sunucularından araçlar kullanabilir. Zoo'ya daha fazla yetenek kazandırır. Ekstra araçları kullanmayacaksan, API token maliyetini azaltmak için bunu KAPAT." }, - "enableServerCreation": { - "title": "MCP Sunucu Oluşturmayı Etkinleştir", - "description": "Bunu AÇ, Zoo'nun <1>yeni özel MCP sunucuları oluşturmanda sana yardımcı olmasını sağlar. <0>Sunucu oluşturma hakkında bilgi al", - "hint": "İpucu: API token maliyetini azaltmak için Zoo'dan yeni bir MCP sunucusu oluşturmasını istemediğinde bu ayarı kapat." - }, "editGlobalMCP": "Global MCP'yi Düzenle", "editProjectMCP": "Proje MCP'sini Düzenle", "learnMoreEditingSettings": "MCP ayar dosyalarını düzenleme hakkında daha fazla bilgi", diff --git a/webview-ui/src/i18n/locales/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index 49becffa6f..c5bb53c7e1 100644 --- a/webview-ui/src/i18n/locales/vi/mcp.json +++ b/webview-ui/src/i18n/locales/vi/mcp.json @@ -8,11 +8,6 @@ "title": "Bật máy chủ MCP", "description": "Bật lên để Zoo dùng công cụ từ các máy chủ MCP đã kết nối. Zoo sẽ có nhiều khả năng hơn. Nếu không dùng các công cụ này, hãy tắt để tiết kiệm chi phí token API." }, - "enableServerCreation": { - "title": "Bật tạo máy chủ MCP", - "description": "Bật lên để Zoo giúp bạn tạo <1>máy chủ MCP mới tuỳ chỉnh. <0>Tìm hiểu về tạo máy chủ", - "hint": "Mẹo: Để giảm chi phí token API, hãy tắt khi không cần Zoo tạo máy chủ MCP mới." - }, "editGlobalMCP": "Chỉnh sửa MCP toàn cục", "editProjectMCP": "Chỉnh sửa MCP dự án", "learnMoreEditingSettings": "Tìm hiểu thêm về chỉnh sửa file cài đặt MCP", diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index 652ab77dec..070a3a9e3b 100644 --- a/webview-ui/src/i18n/locales/zh-CN/mcp.json +++ b/webview-ui/src/i18n/locales/zh-CN/mcp.json @@ -8,11 +8,6 @@ "title": "启用 MCP 服务器", "description": "开启后 Zoo 可用已连接 MCP 服务器的工具,能力更强。不用这些工具时建议关闭,节省 API Token 费用。" }, - "enableServerCreation": { - "title": "启用 MCP 服务器创建", - "description": "开启后 Zoo 可帮你创建<1>新自定义 MCP 服务器。<0>了解服务器创建", - "hint": "提示:不需要 Zoo 创建新 MCP 服务器时建议关闭,减少 API Token 费用。" - }, "editGlobalMCP": "编辑全局 MCP", "editProjectMCP": "编辑项目 MCP", "learnMoreEditingSettings": "了解如何编辑 MCP 设置文件", diff --git a/webview-ui/src/i18n/locales/zh-TW/mcp.json b/webview-ui/src/i18n/locales/zh-TW/mcp.json index ad6e67d22c..8a416d2992 100644 --- a/webview-ui/src/i18n/locales/zh-TW/mcp.json +++ b/webview-ui/src/i18n/locales/zh-TW/mcp.json @@ -8,11 +8,6 @@ "title": "啟用 MCP 伺服器", "description": "啟用此選項後,Zoo 將可使用已連線 MCP 伺服器所提供的工具,進一步提升功能。如果您暫無使用這些額外工具的需求,建議關閉此選項以協助降低 API Token 費用。" }, - "enableServerCreation": { - "title": "啟用 MCP 伺服器建立功能", - "description": "啟用此選項後,Zoo 可協助您建立<1>全新自訂 MCP 伺服器。<0>深入了解伺服器建立流程", - "hint": "提示:若您暫無建立新 MCP 伺服器的需求,建議停用此設定,以協助降低 API Token 費用。" - }, "editGlobalMCP": "編輯全域 MCP", "editProjectMCP": "編輯專案 MCP", "refreshMCP": "重新整理 MCP 伺服器", From c24a8c54ff56cf7f57d50cd10f53a7fbe6dfc6be Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Wed, 22 Jul 2026 04:55:37 +0300 Subject: [PATCH 47/89] feat(types): centralize provider identifiers (#952) --- .../__tests__/provider-identifiers.test.ts | 123 ++++++++++++++++++ packages/types/src/index.ts | 1 + packages/types/src/provider-identifiers.ts | 57 ++++++++ packages/types/src/provider-settings.ts | 48 ++----- 4 files changed, 192 insertions(+), 37 deletions(-) create mode 100644 packages/types/src/__tests__/provider-identifiers.test.ts create mode 100644 packages/types/src/provider-identifiers.ts diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts new file mode 100644 index 0000000000..477f26dc17 --- /dev/null +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -0,0 +1,123 @@ +import { + isProviderName, + isRetiredProvider, + providerIdentifiers, + providerNames, + providerNamesSchema, + providerNamesWithRetiredSchema, + retiredProviderIdentifiers, + retiredProviderNames, + retiredProviderNamesSchema, +} from "../index.js" + +const expectedProviderIdentifiers = [ + "openrouter", + "vercel-ai-gateway", + "zoo-gateway", + "litellm", + "requesty", + "unbound", + "poe", + "deepseek", + "opencode-go", + "kenari", + "ollama", + "lmstudio", + "vscode-lm", + "openai", + "fake-ai", + "anthropic", + "bedrock", + "baseten", + "fireworks", + "friendli", + "gemini", + "gemini-cli", + "mistral", + "moonshot", + "minimax", + "mimo", + "openai-codex", + "openai-native", + "qwen-code", + "sambanova", + "vertex", + "xai", + "zai", +] + +const expectedRetiredProviderIdentifiers = [ + "cerebras", + "chutes", + "deepinfra", + "doubao", + "featherless", + "groq", + "huggingface", + "io-intelligence", + "roo", +] + +describe("provider identifiers", () => { + it("preserves active provider serialized values", () => { + const identifiers = Object.values(providerIdentifiers) + + expect(identifiers).toEqual(expectedProviderIdentifiers) + expect(new Set(identifiers).size).toBe(identifiers.length) + }) + + it("preserves retired provider serialized values", () => { + const identifiers = Object.values(retiredProviderIdentifiers) + + expect(identifiers).toEqual(expectedRetiredProviderIdentifiers) + expect(new Set(identifiers).size).toBe(identifiers.length) + }) + + it("preserves provider-settings compatibility exports", () => { + const identifiers = Object.values(providerIdentifiers) + const retiredIdentifiers = Object.values(retiredProviderIdentifiers) + + expect(providerNames).toEqual(identifiers) + expect(retiredProviderNames).toEqual(retiredIdentifiers) + }) + + it("keeps active and retired providers separate", () => { + const activeIdentifiers = new Set(Object.values(providerIdentifiers)) + const retiredIdentifiers = Object.values(retiredProviderIdentifiers) + + expect(retiredIdentifiers.every((identifier) => !activeIdentifiers.has(identifier))).toBe(true) + }) + + it("derives runtime validation from the canonical registries", () => { + for (const identifier of expectedProviderIdentifiers) { + expect(providerNamesSchema.safeParse(identifier).success).toBe(true) + expect(providerNamesWithRetiredSchema.safeParse(identifier).success).toBe(true) + } + + for (const identifier of expectedRetiredProviderIdentifiers) { + expect(providerNamesSchema.safeParse(identifier).success).toBe(false) + expect(retiredProviderNamesSchema.safeParse(identifier).success).toBe(true) + expect(providerNamesWithRetiredSchema.safeParse(identifier).success).toBe(true) + } + + expect(providerNamesSchema.safeParse("unknown-provider").success).toBe(false) + expect(retiredProviderNamesSchema.safeParse("unknown-provider").success).toBe(false) + expect(providerNamesWithRetiredSchema.safeParse("unknown-provider").success).toBe(false) + }) + + it("preserves provider-settings type guards", () => { + for (const identifier of expectedProviderIdentifiers) { + expect(isProviderName(identifier)).toBe(true) + expect(isRetiredProvider(identifier)).toBe(false) + } + + for (const identifier of expectedRetiredProviderIdentifiers) { + expect(isProviderName(identifier)).toBe(false) + expect(isRetiredProvider(identifier)).toBe(true) + } + + expect(isProviderName("unknown-provider")).toBe(false) + expect(isProviderName(undefined)).toBe(false) + expect(isRetiredProvider("unknown-provider")).toBe(false) + }) +}) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index eaa419b1af..82588ae537 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -18,6 +18,7 @@ export * from "./mcp.js" export * from "./message.js" export * from "./mode.js" export * from "./model.js" +export * from "./provider-identifiers.js" export * from "./provider-settings.js" export * from "./task.js" export * from "./todo.js" diff --git a/packages/types/src/provider-identifiers.ts b/packages/types/src/provider-identifiers.ts new file mode 100644 index 0000000000..30bfb9d17f --- /dev/null +++ b/packages/types/src/provider-identifiers.ts @@ -0,0 +1,57 @@ +/** + * Canonical provider identifiers. + * + * Values in this registry are persisted in settings and must remain stable. + */ +export const providerIdentifiers = { + openrouter: "openrouter", + vercelAiGateway: "vercel-ai-gateway", + zooGateway: "zoo-gateway", + litellm: "litellm", + requesty: "requesty", + unbound: "unbound", + poe: "poe", + deepseek: "deepseek", + opencodeGo: "opencode-go", + kenari: "kenari", + ollama: "ollama", + lmstudio: "lmstudio", + vscodeLm: "vscode-lm", + openai: "openai", + fakeAi: "fake-ai", + anthropic: "anthropic", + bedrock: "bedrock", + baseten: "baseten", + fireworks: "fireworks", + friendli: "friendli", + gemini: "gemini", + geminiCli: "gemini-cli", + mistral: "mistral", + moonshot: "moonshot", + minimax: "minimax", + mimo: "mimo", + openaiCodex: "openai-codex", + openaiNative: "openai-native", + qwenCode: "qwen-code", + sambanova: "sambanova", + vertex: "vertex", + xai: "xai", + zai: "zai", +} as const + +export type ProviderIdentifier = (typeof providerIdentifiers)[keyof typeof providerIdentifiers] + +/** Provider identifiers retained only for compatibility with existing settings. */ +export const retiredProviderIdentifiers = { + cerebras: "cerebras", + chutes: "chutes", + deepinfra: "deepinfra", + doubao: "doubao", + featherless: "featherless", + groq: "groq", + huggingface: "huggingface", + ioIntelligence: "io-intelligence", + roo: "roo", +} as const + +export type RetiredProviderIdentifier = (typeof retiredProviderIdentifiers)[keyof typeof retiredProviderIdentifiers] diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 721b413416..c9b7c01045 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -2,6 +2,12 @@ import { z } from "zod" import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderIdentifier, + type RetiredProviderIdentifier, +} from "./provider-identifiers.js" import { anthropicModels, basetenModels, @@ -111,32 +117,7 @@ export const isFauxProvider = (key: string): key is FauxProvider => fauxProvider * ProviderName */ -export const providerNames = [ - ...dynamicProviders, - ...localProviders, - ...internalProviders, - ...customProviders, - ...fauxProviders, - "anthropic", - "bedrock", - "baseten", - "deepseek", - "fireworks", - "friendli", - "gemini", - "gemini-cli", - "mistral", - "moonshot", - "minimax", - "mimo", - "openai-codex", - "openai-native", - "qwen-code", - "sambanova", - "vertex", - "xai", - "zai", -] as const +export const providerNames = Object.values(providerIdentifiers) as [ProviderIdentifier, ...ProviderIdentifier[]] export const providerNamesSchema = z.enum(providerNames) @@ -149,17 +130,10 @@ export const isProviderName = (key: unknown): key is ProviderName => * RetiredProviderName */ -export const retiredProviderNames = [ - "cerebras", - "chutes", - "deepinfra", - "doubao", - "featherless", - "groq", - "huggingface", - "io-intelligence", - "roo", -] as const +export const retiredProviderNames = Object.values(retiredProviderIdentifiers) as [ + RetiredProviderIdentifier, + ...RetiredProviderIdentifier[], +] export const retiredProviderNamesSchema = z.enum(retiredProviderNames) From ae89dbd7c5b4bc42370c253c10a92da5f060e132 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:53:56 +0000 Subject: [PATCH 48/89] =?UTF-8?q?feat(subtasks):=20abandon=20interrupted?= =?UTF-8?q?=20subtask=20=E2=80=94=20sever=20parent-child=20link=20and=20su?= =?UTF-8?q?rface=20delegation=20status=20(#935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(task-lifecycle): explicit interrupted status + delegation state machine * feat(history-ui): delegation status badges in history list * fix(ci): add dedicated fixtures for abandon e2e test and add missing i18n keys * test: cover evictCurrentTask, markDelegatedChildInterrupted, and onTaskCompleted paths * fix: address CodeRabbit review - guard active-only in markDelegatedChildInterrupted, evict on disposal, fix delegateParentAndOpenChild TOCTOU, translate i18n keys * fix(e2e): allow parent status to be active or completed after child delegation --------- Co-authored-by: Naved Merchant --- apps/vscode-e2e/src/fixtures/subtasks.ts | 71 ++ apps/vscode-e2e/src/suite/subtasks.test.ts | 170 +++- packages/types/src/api.ts | 8 + packages/types/src/vscode-extension-host.ts | 1 + src/__tests__/abandonSubtask.spec.ts | 327 ++++++++ src/__tests__/api-subtask.spec.ts | 102 +++ src/__tests__/helpers/provider-stub.ts | 3 +- .../history-resume-delegation.spec.ts | 2 +- src/__tests__/provider-delegation.spec.ts | 115 ++- .../removeClineFromStack-delegation.spec.ts | 763 +++++++++++++----- src/__tests__/single-open-invariant.spec.ts | 20 +- .../__tests__/registerCommands.spec.ts | 18 + src/activate/registerCommands.ts | 2 +- src/core/webview/ClineProvider.ts | 348 +++++--- .../ClineProvider.flicker-free-cancel.spec.ts | 99 +-- .../ClineProvider.taskHistory.spec.ts | 67 ++ ...bviewMessageHandler.abandonSubtask.spec.ts | 53 ++ src/core/webview/webviewMessageHandler.ts | 9 + src/extension/api.ts | 8 +- webview-ui/src/components/chat/TaskHeader.tsx | 33 +- .../chat/__tests__/TaskHeader.spec.tsx | 54 ++ .../src/components/history/SubtaskRow.tsx | 4 + .../src/components/history/TaskItemFooter.tsx | 8 + .../components/history/TaskStatusBadge.tsx | 36 + .../history/__tests__/TaskItemFooter.spec.tsx | 20 + webview-ui/src/i18n/locales/ca/chat.json | 4 +- webview-ui/src/i18n/locales/ca/history.json | 4 +- webview-ui/src/i18n/locales/de/chat.json | 4 +- webview-ui/src/i18n/locales/de/history.json | 4 +- webview-ui/src/i18n/locales/en/chat.json | 4 +- webview-ui/src/i18n/locales/en/history.json | 4 +- webview-ui/src/i18n/locales/es/chat.json | 4 +- webview-ui/src/i18n/locales/es/history.json | 4 +- webview-ui/src/i18n/locales/fr/chat.json | 4 +- webview-ui/src/i18n/locales/fr/history.json | 4 +- webview-ui/src/i18n/locales/hi/chat.json | 4 +- webview-ui/src/i18n/locales/hi/history.json | 4 +- webview-ui/src/i18n/locales/id/chat.json | 4 +- webview-ui/src/i18n/locales/id/history.json | 4 +- webview-ui/src/i18n/locales/it/chat.json | 4 +- webview-ui/src/i18n/locales/it/history.json | 4 +- webview-ui/src/i18n/locales/ja/chat.json | 4 +- webview-ui/src/i18n/locales/ja/history.json | 4 +- webview-ui/src/i18n/locales/ko/chat.json | 4 +- webview-ui/src/i18n/locales/ko/history.json | 4 +- webview-ui/src/i18n/locales/nl/chat.json | 4 +- webview-ui/src/i18n/locales/nl/history.json | 4 +- webview-ui/src/i18n/locales/pl/chat.json | 4 +- webview-ui/src/i18n/locales/pl/history.json | 4 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 4 +- .../src/i18n/locales/pt-BR/history.json | 4 +- webview-ui/src/i18n/locales/ru/chat.json | 4 +- webview-ui/src/i18n/locales/ru/history.json | 4 +- webview-ui/src/i18n/locales/tr/chat.json | 4 +- webview-ui/src/i18n/locales/tr/history.json | 4 +- webview-ui/src/i18n/locales/vi/chat.json | 4 +- webview-ui/src/i18n/locales/vi/history.json | 4 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 4 +- .../src/i18n/locales/zh-CN/history.json | 4 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 4 +- .../src/i18n/locales/zh-TW/history.json | 4 +- 61 files changed, 2030 insertions(+), 455 deletions(-) create mode 100644 src/__tests__/abandonSubtask.spec.ts create mode 100644 src/__tests__/api-subtask.spec.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts create mode 100644 webview-ui/src/components/history/TaskStatusBadge.tsx diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index 721af81630..5a39577dd9 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -32,6 +32,14 @@ export const SUBTASK_API_HANG_RESUME_MESSAGE = "Continue after provider hang." export const SUBTASK_API_HANG_CHILD_RESULT = "Hung child completed" export const SUBTASK_API_HANG_PARENT_RESULT = "API hang parent resumed" +// Abandon-subtask scenario (#559) — separate markers to avoid sequenceIndex collisions with the +// interrupted-child-resumes tests above, which exhaust the sequence count for INTERRUPT markers. +const SUBTASK_ABANDON_PARENT_MARKER = "SUBTASK_PARENT_ABANDON_SEVER" +const SUBTASK_ABANDON_CHILD_MARKER = "SUBTASK_CHILD_ABANDON_SEVER" +const SUBTASK_ABANDON_CHILD_PROMPT = `${SUBTASK_ABANDON_CHILD_MARKER}: Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.` +export const SUBTASK_ABANDON_PARENT_PROMPT = `${SUBTASK_ABANDON_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_ABANDON_CHILD_PROMPT}" Do not answer directly.` +export const SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER = "9" + const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${SUBTASK_XPROFILE_SAME_CHILD_MARKER}: Complete immediately with the exact result "Same-profile child completed".` const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER}: Complete immediately with the exact result "Different-profile child completed".` export const SUBTASK_XPROFILE_PARENT_PROMPT = `${SUBTASK_XPROFILE_PARENT_MARKER}: First use new_task to create a code-mode subtask with this exact message: "${SUBTASK_XPROFILE_SAME_CHILD_PROMPT}" After it returns, create an ask-mode subtask with the next instructions you receive.` @@ -437,4 +445,67 @@ export function addSubtaskFixtures(mock: InstanceType) { ], }, }) + + // Abandon-subtask scenario (#559) + mock.addFixture({ + match: { + userMessage: new RegExp(SUBTASK_ABANDON_PARENT_MARKER), + sequenceIndex: 0, + }, + response: { + toolCalls: [ + { + name: "new_task", + arguments: JSON.stringify({ + mode: "ask", + message: SUBTASK_ABANDON_CHILD_PROMPT, + }), + id: "call_abandon_parent_new_task_001", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SUBTASK_ABANDON_CHILD_MARKER]) && + !requestContains(req, [SUBTASK_ABANDON_PARENT_MARKER]) && + !requestContains(req, ["call_abandon_child_followup_001"]) && + !requestContains(req, [`\\n${SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER}\\n`]), + }, + response: { + toolCalls: [ + { + name: "ask_followup_question", + arguments: JSON.stringify({ + question: "What is the square root of 81?", + follow_up: [{ text: SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER }], + }), + id: "call_abandon_child_followup_001", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + toolResultContains(req, "call_abandon_child_followup_001", [SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER]) || + requestContains(req, ["call_abandon_child_followup_001", SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER]) || + requestContains(req, [ + SUBTASK_ABANDON_CHILD_MARKER, + `\\n${SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER}\\n`, + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER }), + id: "call_abandon_child_completion_002", + }, + ], + }, + }) } diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 3a81b8b492..661d7f0e2d 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -5,8 +5,10 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { setDefaultSuiteTimeout } from "./test-utils" import { sleep, waitFor, waitUntilCompleted } from "./utils" import { - SUBTASK_API_HANG_CHILD_RESULT, + SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER, + SUBTASK_ABANDON_PARENT_PROMPT, SUBTASK_API_HANG_CHILD_MARKER, + SUBTASK_API_HANG_CHILD_RESULT, SUBTASK_API_HANG_PARENT_MARKER, SUBTASK_API_HANG_PARENT_PROMPT, SUBTASK_API_HANG_PARENT_RESULT, @@ -271,7 +273,10 @@ suite("Roo Code Subtasks", function () { const parent = await api.getTaskHistoryItem(parentTaskId) assert.ok(parent, "Parent history item should exist") - assert.strictEqual(parent.status, "active", "Parent status should be 'active' after child completes") + assert.ok( + parent.status === "active" || parent.status === "completed", + `Parent status should be 'active' or 'completed' after child completes (got '${parent.status}')`, + ) assert.strictEqual(parent.awaitingChildId, undefined, "Parent awaitingChildId should be cleared") assert.strictEqual(parent.delegatedToId, undefined, "Parent delegatedToId should be cleared") assert.strictEqual(parent.completedByChildId, childTaskId, "Parent completedByChildId should be the child") @@ -769,4 +774,165 @@ suite("Roo Code Subtasks", function () { await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) } }) + + // Issue #559: explicit "Abandon subtask" action. Unlike cancellation alone (which leaves + // the child "interrupted" and the parent "delegated" so the child can still resume and + // report back), abandoning severs the link outright: the parent goes back to "active" and + // the child's parentTaskId/rootTaskId are cleared so a later resume can never reattach it. + test("abandoning an interrupted subtask severs the parent-child link", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + api.on(RooCodeEventName.Message, messageHandler) + + try { + const parentTaskId = await api.startNewTask({ + configuration: { + mode: "ask", + alwaysAllowModeSwitch: true, + alwaysAllowSubtasks: true, + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SUBTASK_ABANDON_PARENT_PROMPT, + }) + + let childTaskId: string | undefined + await waitFor(() => { + const stack = api.getCurrentTaskStack() + const current = stack[stack.length - 1] + if (current && current !== parentTaskId) { + childTaskId = current + return true + } + return false + }) + + await waitFor(() => asks[childTaskId!]?.some(({ ask }) => ask === "followup") ?? false) + await waitFor(async () => (await api.getTaskApiConversationHistoryLength(childTaskId!)) > 0) + + // Cancel the child — marked "interrupted", parent stays "delegated". + await api.cancelCurrentTask() + + await waitFor(() => api.getCurrentTaskStack().at(-1) === childTaskId) + await waitFor( + () => asks[childTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + ) + + const interruptedChild = await api.getTaskHistoryItem(childTaskId!) + assert.strictEqual(interruptedChild?.status, "interrupted", "Child should be marked interrupted") + + const delegatedParent = await api.getTaskHistoryItem(parentTaskId) + assert.strictEqual(delegatedParent?.status, "delegated", "Parent should still be delegated before abandon") + assert.strictEqual( + delegatedParent?.awaitingChildId, + childTaskId, + "Parent should await the interrupted child", + ) + + // The interrupted child is the live/open task at this point (cancelTask rehydrates + // it onto the stack). Abandon must close that live instance before severing the + // persisted link — otherwise a later save on the still-open child would rebuild + // parentTaskId/rootTaskId from its live (readonly) fields and silently reattach it. + const abandoned = await api.abandonSubtask(childTaskId!) + assert.strictEqual(abandoned, true, "abandonSubtask should report the link was severed") + + await waitFor(() => api.getCurrentTaskStack().at(-1) !== childTaskId) + + const parentAfterAbandon = await api.getTaskHistoryItem(parentTaskId) + assert.strictEqual(parentAfterAbandon?.status, "active", "Parent should return to active after abandon") + assert.strictEqual( + parentAfterAbandon?.awaitingChildId, + undefined, + "Parent awaitingChildId should be cleared", + ) + assert.strictEqual(parentAfterAbandon?.delegatedToId, undefined, "Parent delegatedToId should be cleared") + + const childAfterAbandon = await api.getTaskHistoryItem(childTaskId!) + // The child's own status is left untouched (VALID_TRANSITIONS only allows interrupted → completed); + // only its parent/root links are cleared so it can never reattach to the parent again. + assert.strictEqual(childAfterAbandon?.status, "interrupted", "Child status stays interrupted") + assert.strictEqual(childAfterAbandon?.parentTaskId, undefined, "Child parentTaskId should be cleared") + assert.strictEqual(childAfterAbandon?.rootTaskId, undefined, "Child rootTaskId should be cleared") + + // A second abandon call is a no-op since the parent is no longer delegated to this child. + const secondAbandon = await api.abandonSubtask(childTaskId!) + assert.strictEqual(secondAbandon, false, "Second abandonSubtask call should be a no-op") + + // Resume and complete the abandoned child — it must NOT reopen or reattach to the + // parent. Before the abandon fix, a subsequent save on the still-live child could + // silently rewrite its persisted parentTaskId back to the parent; this proves the + // link stays severed all the way through a real resume/save/complete cycle. + // api.resumeTask() re-instantiates the child from history, which re-raises its own + // "resume_task" ask; answering it with the follow-up answer (same pattern the sibling + // "cancelled child completes and reopens parent" test above uses) both resumes the + // task and supplies the answer the re-asked follow-up question is waiting for. + // asks[childTaskId] already holds the earlier resume_task ask from the pre-abandon + // cancellation, so the wait below must look for a NEW one, not just any occurrence. + const askCountBeforeResume = asks[childTaskId!]?.length ?? 0 + await api.resumeTask(childTaskId!) + await waitFor(() => + (asks[childTaskId!] ?? []) + .slice(askCountBeforeResume) + .some(({ type, ask }) => type === "ask" && ask === "resume_task"), + ) + + const completedChildTaskId = await waitUntilCompleted({ + api, + start: async () => { + await api.sendMessage(SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER) + return childTaskId! + }, + }) + + assert.strictEqual( + completedChildTaskId, + childTaskId, + "The abandoned child itself should be the task that completes, not the parent", + ) + assert.strictEqual( + says[parentTaskId]?.find(({ say }) => say === "completion_result"), + undefined, + "Parent must never complete/reopen after its abandoned child resumes and completes", + ) + + const parentAfterChildCompletes = await api.getTaskHistoryItem(parentTaskId) + assert.strictEqual( + parentAfterChildCompletes?.status, + "active", + "Parent status must remain untouched by the abandoned child's completion", + ) + assert.strictEqual( + parentAfterChildCompletes?.awaitingChildId, + undefined, + "Parent must not start awaiting the abandoned child again", + ) + + const childAfterCompletion = await api.getTaskHistoryItem(childTaskId!) + assert.strictEqual( + childAfterCompletion?.parentTaskId, + undefined, + "Child parentTaskId must still be cleared after it completes on its own — " + + "proves the live-instance save did not resurrect the old link", + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) + } + }) }) diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 2dbaae7920..89e9c8bc2b 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -64,6 +64,14 @@ export interface RooCodeAPI extends EventEmitter { * Cancels the current task. */ cancelCurrentTask(): Promise + /** + * Severs the delegated parent-child link for an interrupted (cancelled, not running) + * subtask, so the parent stops waiting on it and returns to "active". No-op (returns + * false) unless the child is interrupted and its parent is still delegated to it. + * @param childTaskId The ID of the child (subtask) to abandon. + * @returns True if the link was severed, false if there was nothing to abandon. + */ + abandonSubtask(childTaskId: string): Promise /** * Sends a message to the current task. * @param message Optional message to send. diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 75a72accde..9bd53f907a 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -460,6 +460,7 @@ export interface WebviewMessage { | "shareCurrentTask" | "showTaskWithId" | "deleteTaskWithId" + | "abandonSubtaskWithId" | "exportTaskWithId" | "importSettings" | "exportSettings" diff --git a/src/__tests__/abandonSubtask.spec.ts b/src/__tests__/abandonSubtask.spec.ts new file mode 100644 index 0000000000..ded593c596 --- /dev/null +++ b/src/__tests__/abandonSubtask.spec.ts @@ -0,0 +1,327 @@ +// npx vitest run __tests__/abandonSubtask.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import type { HistoryItem } from "@roo-code/types" + +import { ClineProvider } from "../core/webview/ClineProvider" +import { makeProviderStub } from "./helpers/provider-stub" + +/** + * Minimal taskHistoryStore stub whose atomicUpdatePair calls both updaters + * against an in-memory item map and resolves, simulating the happy-path atomic write. + */ +function makeTaskHistoryStoreStub(childItem: Record, parentItem: Record) { + const itemMap = new Map>([ + [childItem.id!, childItem], + [parentItem.id!, parentItem], + ]) + + const atomicUpdatePair = vi.fn( + async ( + firstId: string, + secondId: string, + firstUpdater: (h: HistoryItem) => HistoryItem, + secondUpdater: (h: HistoryItem) => HistoryItem, + ) => { + itemMap.set(firstId, firstUpdater(itemMap.get(firstId) as HistoryItem)) + itemMap.set(secondId, secondUpdater(itemMap.get(secondId) as HistoryItem)) + return [] + }, + ) + + return { + atomicUpdatePair, + get: vi.fn((id: string) => itemMap.get(id)), + } +} + +describe("ClineProvider.abandonSubtask()", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("severs the link: parent → active (awaitingChildId/delegatedToId cleared), child loses parentTaskId/rootTaskId", async () => { + const childHistoryItem = { + id: "child-1", + status: "interrupted", + parentTaskId: "parent-1", + rootTaskId: "parent-1", + ts: Date.now(), + task: "Child task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const parentHistoryItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + childIds: ["child-1"], + ts: Date.now(), + task: "Parent task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + + const provider = makeProviderStub({ + getTaskWithId, + getCurrentTask: vi.fn(() => undefined), + taskHistoryStore, + isViewLaunched: true, + postMessageToWebview: vi.fn().mockResolvedValue(undefined), + } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(true) + expect(taskHistoryStore.atomicUpdatePair).toHaveBeenCalledTimes(1) + const [firstId, secondId] = taskHistoryStore.atomicUpdatePair.mock.calls[0] + expect(firstId).toBe("child-1") + expect(secondId).toBe("parent-1") + + const updatedChild = taskHistoryStore.get("child-1") + expect(updatedChild).toEqual( + expect.objectContaining({ + id: "child-1", + status: "interrupted", + parentTaskId: undefined, + rootTaskId: undefined, + }), + ) + + const updatedParent = taskHistoryStore.get("parent-1") + expect(updatedParent).toEqual( + expect.objectContaining({ + id: "parent-1", + status: "active", + awaitingChildId: undefined, + delegatedToId: undefined, + }), + ) + + // Guarded against a stale in-flight completion reattaching the child. + expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + + // Both updated items broadcast to the webview with the actual severed-link field values, + // not just matching IDs — a stale/pre-abandon payload would still match an id-only assertion. + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "taskHistoryItemUpdated", + taskHistoryItem: expect.objectContaining({ + id: "child-1", + status: "interrupted", + parentTaskId: undefined, + rootTaskId: undefined, + }), + }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "taskHistoryItemUpdated", + taskHistoryItem: expect.objectContaining({ + id: "parent-1", + status: "active", + awaitingChildId: undefined, + delegatedToId: undefined, + }), + }) + }) + + it("closes the live child instance before severing the link, so a later save cannot reattach it", async () => { + // An interrupted child is commonly still the live/open task (cancelTask rehydrates it + // onto the stack). If abandon doesn't close it first, Task#saveClineMessages() would + // rebuild parentTaskId/rootTaskId from the live task's readonly fields on its next save + // and silently reattach the child. removeClineFromStack() must run before atomicUpdatePair. + const childHistoryItem = { + id: "child-1", + status: "interrupted", + parentTaskId: "parent-1", + rootTaskId: "parent-1", + ts: Date.now(), + task: "Child task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const parentHistoryItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + childIds: ["child-1"], + ts: Date.now(), + task: "Parent task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const removeClineFromStack = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + getTaskWithId, + getCurrentTask: vi.fn(() => ({ taskId: "child-1" })), + removeClineFromStack, + taskHistoryStore, + isViewLaunched: false, + } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(true) + expect(removeClineFromStack).toHaveBeenCalledWith() + // The live child must be closed before the persisted link is severed. + const closeCallOrder = removeClineFromStack.mock.invocationCallOrder[0] + const atomicCallOrder = taskHistoryStore.atomicUpdatePair.mock.invocationCallOrder[0] + expect(closeCallOrder).toBeLessThan(atomicCallOrder) + }) + + it("does not attempt to close the live instance when the child is not the current task", async () => { + const childHistoryItem = { id: "child-1", status: "interrupted", parentTaskId: "parent-1" } + const parentHistoryItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const removeClineFromStack = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + getTaskWithId, + getCurrentTask: vi.fn(() => ({ taskId: "some-other-task" })), + removeClineFromStack, + taskHistoryStore, + isViewLaunched: false, + } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(true) + expect(removeClineFromStack).not.toHaveBeenCalled() + }) + + it("returns false and does not modify state when the child is not interrupted (e.g. still active)", async () => { + const childHistoryItem = { id: "child-1", status: "active", parentTaskId: "parent-1" } + const parentHistoryItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(false) + expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() + }) + + it("returns false when the child completes between the initial check and lock acquisition (TOCTOU)", async () => { + // The initial status check reads the child as interrupted, but by the time the + // per-parent lock is acquired, a concurrent resume-and-complete has already + // transitioned it. The in-lock re-check must catch this and bail out. + const childHistoryItem = { id: "child-1", status: "interrupted", parentTaskId: "parent-1" } + const parentHistoryItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore: any = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + // Simulate the concurrent completion landing right before the in-lock re-check runs. + taskHistoryStore.get = vi.fn((id: string) => + id === "child-1" ? { ...childHistoryItem, status: "completed" as const } : parentHistoryItem, + ) + + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(false) + expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() + }) + + it("returns false and does not modify state when the child has no parentTaskId", async () => { + const getTaskWithId = vi.fn().mockResolvedValue({ historyItem: { id: "standalone-1", status: "active" } }) + const provider = makeProviderStub({ getTaskWithId } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "standalone-1") + + expect(result).toBe(false) + }) + + it("returns false and does not touch history when parent is no longer delegated to this child", async () => { + const childHistoryItem = { id: "child-1", status: "interrupted", parentTaskId: "parent-1" } + const parentHistoryItem = { id: "parent-1", status: "active", awaitingChildId: undefined } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(false) + expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() + }) + + it("returns false when awaitingChildId points at a different child", async () => { + const childHistoryItem = { id: "child-1", status: "interrupted", parentTaskId: "parent-1" } + const parentHistoryItem = { id: "parent-1", status: "delegated", awaitingChildId: "child-OTHER" } + + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === "child-1") return { historyItem: childHistoryItem } + if (id === "parent-1") return { historyItem: parentHistoryItem } + throw new Error("Task not found") + }) + + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + + const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + + expect(result).toBe(false) + expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/api-subtask.spec.ts b/src/__tests__/api-subtask.spec.ts new file mode 100644 index 0000000000..2b5a1a86e9 --- /dev/null +++ b/src/__tests__/api-subtask.spec.ts @@ -0,0 +1,102 @@ +// npx vitest run __tests__/api-subtask.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import { EventEmitter } from "events" + +vi.mock("vscode", () => ({ + workspace: { workspaceFolders: [] }, + window: { + createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }), + }, + env: { language: "en" }, + Uri: { + file: vi.fn((p: string) => ({ fsPath: p })), + parse: vi.fn((s: string) => ({ toString: () => s })), + }, + commands: { registerCommand: vi.fn().mockReturnValue({ dispose: vi.fn() }) }, +})) + +vi.mock("p-wait-for", () => ({ default: vi.fn().mockResolvedValue(undefined) })) + +vi.mock("@roo-code/ipc", () => ({ + IpcServer: class { + listen() {} + on() {} + close() {} + }, +})) + +vi.mock("../services/command/commands", () => ({ getCommands: vi.fn().mockResolvedValue([]) })) + +import { API } from "../extension/api" + +function makeProviderMock() { + const emitter = new EventEmitter() + return { + on: emitter.on.bind(emitter), + off: emitter.off.bind(emitter), + emit: emitter.emit.bind(emitter), + context: { + extensionPath: "/test", + globalStorageUri: { fsPath: "/test/storage" }, + subscriptions: [], + }, + cwd: "/test/cwd", + evictCurrentTask: vi.fn().mockResolvedValue(undefined), + postStateToWebview: vi.fn().mockResolvedValue(undefined), + abandonSubtask: vi.fn().mockResolvedValue(true), + getCurrentTask: vi.fn().mockReturnValue(undefined), + viewLaunched: false, + cancelTask: vi.fn().mockResolvedValue(undefined), + getTaskWithId: vi.fn().mockRejectedValue(new Error("not found")), + taskHistoryStore: { get: vi.fn().mockReturnValue(undefined), getAll: vi.fn().mockReturnValue([]) }, + getCurrentTaskStack: vi.fn().mockReturnValue([]), + getModes: vi.fn().mockResolvedValue([]), + postMessageToWebview: vi.fn().mockResolvedValue(undefined), + } +} + +describe("API.clearCurrentTask()", () => { + let provider: ReturnType + let api: API + + beforeEach(() => { + vi.clearAllMocks() + provider = makeProviderMock() + api = new API({} as any, provider as any) + }) + + it("calls evictCurrentTask then postStateToWebview on sidebarProvider", async () => { + await api.clearCurrentTask() + expect(provider.evictCurrentTask).toHaveBeenCalledTimes(1) + expect(provider.postStateToWebview).toHaveBeenCalledTimes(1) + // evict must come before postState + const evictOrder = provider.evictCurrentTask.mock.invocationCallOrder[0] + const postOrder = provider.postStateToWebview.mock.invocationCallOrder[0] + expect(evictOrder).toBeLessThan(postOrder) + }) +}) + +describe("API.abandonSubtask()", () => { + let provider: ReturnType + let api: API + + beforeEach(() => { + vi.clearAllMocks() + provider = makeProviderMock() + api = new API({} as any, provider as any) + }) + + it("delegates to sidebarProvider.abandonSubtask and returns its result", async () => { + provider.abandonSubtask.mockResolvedValue(true) + const result = await api.abandonSubtask("child-task-1") + expect(provider.abandonSubtask).toHaveBeenCalledWith("child-task-1") + expect(result).toBe(true) + }) + + it("returns false when sidebarProvider.abandonSubtask returns false", async () => { + provider.abandonSubtask.mockResolvedValue(false) + const result = await api.abandonSubtask("child-task-2") + expect(result).toBe(false) + }) +}) diff --git a/src/__tests__/helpers/provider-stub.ts b/src/__tests__/helpers/provider-stub.ts index c9d77ad877..c91016d93a 100644 --- a/src/__tests__/helpers/provider-stub.ts +++ b/src/__tests__/helpers/provider-stub.ts @@ -12,9 +12,10 @@ export function makeProviderStub(stub: T): T { const proto = ClineProvider.prototype as any s.delegationTransitionLocks ??= new Map() s.cancelledDelegationChildIds ??= new Set() - s.cancellingDelegationChildIds ??= new Set() s.log ??= vi.fn() s.taskHistoryStore ??= { get: () => undefined } s.runDelegationTransition = proto.runDelegationTransition.bind(s) + s.removeClineFromStack ??= proto.removeClineFromStack.bind(s) + s.evictCurrentTask ??= proto.evictCurrentTask.bind(s) return s } diff --git a/src/__tests__/history-resume-delegation.spec.ts b/src/__tests__/history-resume-delegation.spec.ts index 13560a7434..9c54ba3301 100644 --- a/src/__tests__/history-resume-delegation.spec.ts +++ b/src/__tests__/history-resume-delegation.spec.ts @@ -168,7 +168,7 @@ describe("History resume delegation - parent metadata transitions", () => { // Verify child closed and parent reopened with updated metadata expect(removeClineFromStack).toHaveBeenCalledTimes(1) - expect(removeClineFromStack).toHaveBeenCalledWith({ skipDelegationRepair: true }) + expect(removeClineFromStack).toHaveBeenCalledWith() expect(createTaskWithHistoryItem).toHaveBeenCalledWith( expect.objectContaining({ status: "active", diff --git a/src/__tests__/provider-delegation.spec.ts b/src/__tests__/provider-delegation.spec.ts index ddc6a3c4f4..0a9c34b979 100644 --- a/src/__tests__/provider-delegation.spec.ts +++ b/src/__tests__/provider-delegation.spec.ts @@ -213,6 +213,117 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { expect(callOrder).toEqual(["createTask", "atomicReadAndUpdate", "child.start"]) }) + it("implicitly severs interrupted awaited child and re-delegates when parent is already delegated", async () => { + const oldChildId = "old-child" + const oldChild = { id: oldChildId, status: "interrupted" } as unknown as HistoryItem + const alreadyDelegatedParent: HistoryItem = { + ...parentHistoryItem, + status: "delegated", + awaitingChildId: oldChildId, + delegatedToId: oldChildId, + childIds: [oldChildId], + } as unknown as HistoryItem + + const taskHistoryStore = makeStoreStub({ + // store returns: parent (delegated), old child (interrupted) + get: vi.fn((id: string) => + id === "parent-1" ? alreadyDelegatedParent : id === oldChildId ? oldChild : undefined, + ), + atomicReadAndUpdate: vi.fn(async (_taskId: string, updater: (h: HistoryItem) => HistoryItem) => { + updater(alreadyDelegatedParent) + return [] + }), + }) + + const provider = { + emit: vi.fn(), + getCurrentTask: vi.fn(() => makeParentTask()), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask: vi.fn().mockResolvedValue({ taskId: "child-2", start: vi.fn() }), + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + isViewLaunched: false, + recentTasksCache: undefined, + taskHistoryStore, + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Continue", + initialTodos: [], + mode: "code", + }) + + // The updater must sever the old link and apply the new delegation + const [, updater] = taskHistoryStore.atomicReadAndUpdate.mock.calls[0] + const result = updater(alreadyDelegatedParent) + expect(result).toMatchObject({ + status: "delegated", + awaitingChildId: "child-2", + delegatedToId: "child-2", + }) + // Old child ID preserved in childIds (audit trail) + expect(result.childIds).toContain(oldChildId) + expect(result.childIds).toContain("child-2") + }) + + it("rejects with 'Cannot re-delegate' when the existing awaited child is still active", async () => { + const oldChildId = "old-child" + const activeChild = { id: oldChildId, status: "active" } as unknown as HistoryItem + const alreadyDelegatedParent: HistoryItem = { + ...parentHistoryItem, + status: "delegated", + awaitingChildId: oldChildId, + delegatedToId: oldChildId, + } as unknown as HistoryItem + + const child = { taskId: "child-2", start: vi.fn() } + const getCurrentTask = vi.fn().mockReturnValue(makeParentTask()) + const createTask = vi.fn().mockImplementation(async () => { + getCurrentTask.mockReturnValue(child) + return child + }) + + const taskHistoryStore = makeStoreStub({ + get: vi.fn((id: string) => + id === "parent-1" ? alreadyDelegatedParent : id === oldChildId ? activeChild : undefined, + ), + // Real atomicReadAndUpdate behaviour: call the updater and propagate any throw + atomicReadAndUpdate: vi.fn(async (_taskId: string, updater: (h: HistoryItem) => HistoryItem) => { + updater(alreadyDelegatedParent) + return [] + }), + }) + + const provider = { + emit: vi.fn(), + getCurrentTask, + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask, + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + deleteTaskWithId: vi.fn().mockResolvedValue(undefined), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: alreadyDelegatedParent }), + createTaskWithHistoryItem: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + isViewLaunched: false, + recentTasksCache: undefined, + taskHistoryStore, + } as unknown as ClineProvider + + await expect( + (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Continue", + initialTodos: [], + mode: "code", + }), + ).rejects.toThrow("Cannot re-delegate") + + // Rollback: child must not have started, and must be cleaned up + expect(child.start).not.toHaveBeenCalled() + expect((provider as any).deleteTaskWithId).toHaveBeenCalledWith("child-2", false) + }) + it("rolls back the paused child and restores the parent when atomicReadAndUpdate fails", async () => { const persistError = new Error("parent metadata persist failed") const parentTask = makeParentTask() @@ -260,8 +371,8 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { ).rejects.toThrow(persistError) expect(childStart).not.toHaveBeenCalled() - expect(removeClineFromStack).toHaveBeenNthCalledWith(1, { skipDelegationRepair: true }) - expect(removeClineFromStack).toHaveBeenNthCalledWith(2, { skipDelegationRepair: true }) + expect(removeClineFromStack).toHaveBeenNthCalledWith(1) + expect(removeClineFromStack).toHaveBeenNthCalledWith(2) expect(deleteTaskWithId).toHaveBeenCalledWith("child-1", false) expect(createTaskWithHistoryItem).toHaveBeenCalledWith(parentHistoryItem) }) diff --git a/src/__tests__/removeClineFromStack-delegation.spec.ts b/src/__tests__/removeClineFromStack-delegation.spec.ts index 6013d8db99..a91900c38a 100644 --- a/src/__tests__/removeClineFromStack-delegation.spec.ts +++ b/src/__tests__/removeClineFromStack-delegation.spec.ts @@ -4,289 +4,358 @@ import { describe, it, expect, vi } from "vitest" import { ClineProvider } from "../core/webview/ClineProvider" import { makeProviderStub } from "./helpers/provider-stub" -describe("ClineProvider.removeClineFromStack() delegation awareness", () => { - /** - * Helper to build a minimal mock provider with a single task on the stack. - * The task's parentTaskId and taskId are configurable. - */ - function buildMockProvider(opts: { - childTaskId: string - parentTaskId?: string - parentHistoryItem?: Record - getTaskWithIdError?: Error - }) { - const childTask = { - taskId: opts.childTaskId, - instanceId: "inst-1", - parentTaskId: opts.parentTaskId, - emit: vi.fn(), - abortTask: vi.fn().mockResolvedValue(undefined), +// After the refactor: removeClineFromStack() is pure lifecycle — it pops, aborts, and +// cleans up listeners. It does NOT mutate delegation metadata. All delegated→active +// transitions are owned by reopenParentFromDelegation() (normal child completion) or +// markDelegatedChildInterrupted() (live eviction via navigation / new-task / clear). + +function buildMockProvider(opts: { + childTaskId: string + parentTaskId?: string + parentHistoryItem?: Record + childStatus?: string +}) { + const childTask = { + taskId: opts.childTaskId, + instanceId: "inst-1", + parentTaskId: opts.parentTaskId, + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === opts.parentTaskId && opts.parentHistoryItem) { + return { historyItem: { ...opts.parentHistoryItem } } } + throw new Error("Task not found") + }) - const updateTaskHistory = vi.fn().mockResolvedValue([]) - const getTaskWithId = opts.getTaskWithIdError - ? vi.fn().mockRejectedValue(opts.getTaskWithIdError) - : vi.fn().mockImplementation(async (id: string) => { - if (id === opts.parentTaskId && opts.parentHistoryItem) { - return { historyItem: { ...opts.parentHistoryItem } } - } - throw new Error("Task not found") - }) + const taskHistoryStoreData: Record = {} + if (opts.childStatus) { + taskHistoryStoreData[opts.childTaskId] = { status: opts.childStatus } + } - const provider = makeProviderStub({ - clineStack: [childTask] as any[], - taskEventListeners: new Map(), - log: vi.fn(), - getTaskWithId, - updateTaskHistory, - }) + const provider = makeProviderStub({ + clineStack: [childTask] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId, + updateTaskHistory, + taskHistoryStore: { get: (id: string) => taskHistoryStoreData[id] }, + }) - return { provider, childTask, updateTaskHistory, getTaskWithId } - } + return { provider, childTask, updateTaskHistory, getTaskWithId } +} + +describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation side effects", () => { + it("pops the task, aborts it, and clears listeners", async () => { + const { provider, childTask } = buildMockProvider({ childTaskId: "child-1" }) + expect(provider.clineStack).toHaveLength(1) + + await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - it("repairs parent metadata (delegated → active) when a delegated child is removed", async () => { + expect(provider.clineStack).toHaveLength(0) + expect(childTask.abortTask).toHaveBeenCalledWith(true) + expect(childTask.emit).toHaveBeenCalledWith(expect.stringContaining("taskUnfocused")) + }) + + it("does NOT mutate parent metadata when a delegated child is popped (repair removed)", async () => { const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ childTaskId: "child-1", parentTaskId: "parent-1", parentHistoryItem: { id: "parent-1", - task: "Parent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, status: "delegated", awaitingChildId: "child-1", delegatedToId: "child-1", - childIds: ["child-1"], }, }) await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - // Stack should be empty after pop - expect(provider.clineStack).toHaveLength(0) - - // Parent lookup should have been called - expect(getTaskWithId).toHaveBeenCalledWith("parent-1") - - // Parent metadata should be repaired - expect(updateTaskHistory).toHaveBeenCalledTimes(1) - const updatedParent = updateTaskHistory.mock.calls[0][0] - expect(updatedParent).toEqual( - expect.objectContaining({ - id: "parent-1", - status: "active", - awaitingChildId: undefined, - delegatedToId: undefined, - }), - ) - - // Log the repair - expect(provider.log).toHaveBeenCalledWith(expect.stringContaining("Repaired parent parent-1 metadata")) - }) - - it("does NOT modify parent metadata when the task has no parentTaskId (non-delegated)", async () => { - const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ - childTaskId: "standalone-1", - // No parentTaskId — this is a top-level task - }) - - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - - // Stack should be empty expect(provider.clineStack).toHaveLength(0) - - // No parent lookup or update should happen + // Navigation/disposal must never silently flip the parent to active expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) - it("does NOT modify parent metadata when awaitingChildId does not match the popped child", async () => { + it("does NOT mutate parent metadata when the child is interrupted", async () => { const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ childTaskId: "child-1", parentTaskId: "parent-1", parentHistoryItem: { id: "parent-1", - task: "Parent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, status: "delegated", - awaitingChildId: "child-OTHER", // different child - delegatedToId: "child-OTHER", - childIds: ["child-OTHER"], + awaitingChildId: "child-1", }, + childStatus: "interrupted", }) await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - // Parent was looked up but should NOT be updated - expect(getTaskWithId).toHaveBeenCalledWith("parent-1") + expect(provider.clineStack).toHaveLength(0) + expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) - it("does NOT modify parent metadata when parent status is not 'delegated'", async () => { + it("does NOT mutate parent metadata for a non-delegated (top-level) task", async () => { const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ - childTaskId: "child-1", - parentTaskId: "parent-1", - parentHistoryItem: { - id: "parent-1", - task: "Parent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - status: "completed", // already completed - awaitingChildId: "child-1", - childIds: ["child-1"], - }, + childTaskId: "standalone-1", }) await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - expect(getTaskWithId).toHaveBeenCalledWith("parent-1") + expect(provider.clineStack).toHaveLength(0) + expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) - it("catches and logs errors during parent metadata repair without blocking the pop", async () => { - const { provider, childTask, updateTaskHistory, getTaskWithId } = buildMockProvider({ - childTaskId: "child-1", - parentTaskId: "parent-1", - getTaskWithIdError: new Error("Storage unavailable"), + it("handles empty stack gracefully", async () => { + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId: vi.fn(), + updateTaskHistory: vi.fn(), }) - // Should NOT throw - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await expect((ClineProvider.prototype as any).removeClineFromStack.call(provider)).resolves.not.toThrow() - // Stack should still be empty (pop was not blocked) - expect(provider.clineStack).toHaveLength(0) + expect((provider as any).getTaskWithId).not.toHaveBeenCalled() + expect((provider as any).updateTaskHistory).not.toHaveBeenCalled() + }) +}) - // The abort should still have been called - expect(childTask.abortTask).toHaveBeenCalledWith(true) +describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", () => { + it("marks an active delegated child interrupted and leaves parent delegated", async () => { + const childTaskId = "child-1" + const parentTaskId = "parent-1" - // Error should be logged as non-fatal - expect(provider.log).toHaveBeenCalledWith( - expect.stringContaining("Failed to repair parent metadata for parent-1 (non-fatal)"), - ) + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === parentTaskId) { + return { + historyItem: { + id: parentTaskId, + status: "delegated", + awaitingChildId: childTaskId, + delegatedToId: childTaskId, + }, + } + } + if (id === childTaskId) { + return { + historyItem: { + id: childTaskId, + status: "active", + parentTaskId, + }, + } + } + throw new Error("Not found") + }) - // No update should have been attempted - expect(updateTaskHistory).not.toHaveBeenCalled() + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId, + updateTaskHistory, + postMessageToWebview, + taskHistoryStore: { + get: (id: string) => (id === childTaskId ? { id: childTaskId, status: "active" } : undefined), + }, + }) + + await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }) + + // Child must be marked interrupted + expect(updateTaskHistory).toHaveBeenCalledWith( + expect.objectContaining({ id: childTaskId, status: "interrupted" }), + ) + // Parent must NOT be touched at all — stays delegated + expect(updateTaskHistory).not.toHaveBeenCalledWith(expect.objectContaining({ id: parentTaskId })) + // Webview must receive correct field name: taskHistoryItem (not historyItem) + expect(postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ + type: "taskHistoryItemUpdated", + taskHistoryItem: expect.objectContaining({ id: childTaskId, status: "interrupted" }), + }), + ) + expect(postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ + type: "taskHistoryItemUpdated", + taskHistoryItem: expect.objectContaining({ id: parentTaskId, status: "delegated" }), + }), + ) }) - it("handles empty stack gracefully", async () => { - const provider = { + it("is a no-op when the child is already interrupted", async () => { + const childTaskId = "child-1" + const parentTaskId = "parent-1" + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + + const provider = makeProviderStub({ clineStack: [] as any[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId: vi.fn(), - updateTaskHistory: vi.fn(), - } + updateTaskHistory, + taskHistoryStore: { + get: (id: string) => (id === childTaskId ? { id: childTaskId, status: "interrupted" } : undefined), + }, + }) - // Should not throw - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }) - expect(provider.clineStack).toHaveLength(0) - expect(provider.getTaskWithId).not.toHaveBeenCalled() - expect(provider.updateTaskHistory).not.toHaveBeenCalled() + expect(updateTaskHistory).not.toHaveBeenCalled() }) - it("skips delegation repair when skipDelegationRepair option is true", async () => { - const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ - childTaskId: "child-1", - parentTaskId: "parent-1", - parentHistoryItem: { - id: "parent-1", - task: "Parent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - status: "delegated", - awaitingChildId: "child-1", - delegatedToId: "child-1", - childIds: ["child-1"], + it("is a no-op when parent is no longer delegated to this child", async () => { + const childTaskId = "child-1" + const parentTaskId = "parent-1" + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const getTaskWithId = vi.fn().mockResolvedValue({ + historyItem: { + id: parentTaskId, + status: "active", // already repaired by another path + awaitingChildId: undefined, }, }) - // Call with skipDelegationRepair: true (as delegateParentAndOpenChild would) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider, { skipDelegationRepair: true }) + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId, + updateTaskHistory, + taskHistoryStore: { + get: (id: string) => (id === childTaskId ? { id: childTaskId, status: "active" } : undefined), + }, + }) - // Stack should be empty after pop - expect(provider.clineStack).toHaveLength(0) + await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }) - // Parent lookup should NOT have been called — repair was skipped entirely - expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) - it("does NOT reset grandparent during A→B→C nested delegation transition", async () => { - // Scenario: A delegated to B, B is now delegating to C. - // delegateParentAndOpenChild() pops B via removeClineFromStack({ skipDelegationRepair: true }). - // Grandparent A should remain "delegated" — its metadata must not be repaired. - const grandparentHistory = { - id: "task-A", - task: "Grandparent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - status: "delegated", - awaitingChildId: "task-B", - delegatedToId: "task-B", - childIds: ["task-B"], - } + it("skips the update when cancelTask marks the child interrupted between outer check and lock (TOCTOU)", async () => { + // Outer store returns "active" (fast path passes), but inside the lock the store + // now returns "interrupted" (cancelTask beat us). The in-lock re-check must bail. + const childTaskId = "child-toctou" + const parentTaskId = "parent-1" - const taskB = { - taskId: "task-B", - instanceId: "inst-B", - parentTaskId: "task-A", - emit: vi.fn(), - abortTask: vi.fn().mockResolvedValue(undefined), - } + const updateTaskHistory = vi.fn().mockResolvedValue([]) + let lockAcquired = false const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { - if (id === "task-A") { - return { historyItem: { ...grandparentHistory } } + if (id === parentTaskId) { + return { historyItem: { id: parentTaskId, status: "delegated", awaitingChildId: childTaskId } } } - throw new Error("Task not found") + // Child history fetch inside lock returns interrupted — cancelTask beat us + return { historyItem: { id: childTaskId, status: "interrupted", parentTaskId } } }) - const updateTaskHistory = vi.fn().mockResolvedValue([]) - const provider = { - clineStack: [taskB] as any[], + const provider = makeProviderStub({ + clineStack: [] as any[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, updateTaskHistory, - } + taskHistoryStore: { + // Outer check: "active" (pre-lock); in-lock check reads from taskHistoryStore too + // but the code falls back to getTaskWithId inside the lock when the store shows active. + get: vi.fn((id: string) => { + if (id === childTaskId) { + // After lock acquired, simulate cancelTask flipping to interrupted + return lockAcquired + ? { id: childTaskId, status: "interrupted" } + : { id: childTaskId, status: "active" } + } + return undefined + }), + }, + }) - // Simulate what delegateParentAndOpenChild does: pop B with skipDelegationRepair - await (ClineProvider.prototype as any).removeClineFromStack.call(provider, { skipDelegationRepair: true }) + // Patch runDelegationTransition to set lockAcquired before calling fn + const realRunDelegation = (provider as any).runDelegationTransition.bind(provider) + ;(provider as any).runDelegationTransition = async (_parentId: string, fn: () => Promise) => { + lockAcquired = true + return realRunDelegation(_parentId, fn) + } - // B was popped - expect(provider.clineStack).toHaveLength(0) + await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }) - // Grandparent A should NOT have been looked up or modified - expect(getTaskWithId).not.toHaveBeenCalled() + // Since the in-lock store check returns "interrupted", the code skips updateTaskHistory expect(updateTaskHistory).not.toHaveBeenCalled() + }) + + it("logs and swallows errors from runDelegationTransition", async () => { + const childTaskId = "child-err" + const parentTaskId = "parent-1" + + const log = vi.fn() + const getTaskWithId = vi.fn().mockRejectedValue(new Error("store unavailable")) + + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + log, + getTaskWithId, + updateTaskHistory: vi.fn(), + taskHistoryStore: { + get: (id: string) => (id === childTaskId ? { id: childTaskId, status: "active" } : undefined), + }, + }) + + await expect( + (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }), + ).resolves.not.toThrow() - // Grandparent A's metadata remains intact (delegated, awaitingChildId: task-B) - // The caller (delegateParentAndOpenChild) will update A to point to C separately. + expect(log).toHaveBeenCalledWith(expect.stringContaining("Failed for child")) }) +}) - it("does NOT repair parent when child has 'interrupted' status (cancel already persisted it)", async () => { - // cancelTask() writes child status: "interrupted" and leaves parent "delegated". - // When rehydrate then calls removeClineFromStack, parent must stay delegated. +describe("createTaskWithHistoryItem() navigation — does not mutate delegation state", () => { + it("navigating to a delegated parent while its interrupted child is current leaves parent delegated", async () => { + // This is the core regression: previously removeClineFromStack's repair fired here + // and flipped the parent to active, hiding the Abandon button. const childTaskId = "child-1" const parentTaskId = "parent-1" + const parentHistoryItem = { + id: parentTaskId, + status: "delegated", + awaitingChildId: childTaskId, + delegatedToId: childTaskId, + } + + const childHistoryItem = { + id: childTaskId, + status: "interrupted", + parentTaskId, + } + const childTask = { taskId: childTaskId, instanceId: "inst-child", @@ -297,44 +366,304 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => { const updateTaskHistory = vi.fn().mockResolvedValue([]) const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { - if (id === parentTaskId) { - return { - historyItem: { - id: parentTaskId, - task: "Parent task", - ts: 1000, - number: 1, - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - status: "delegated", - awaitingChildId: childTaskId, - }, - } - } - throw new Error("Task not found") + if (id === parentTaskId) return { historyItem: { ...parentHistoryItem } } + if (id === childTaskId) return { historyItem: { ...childHistoryItem } } + throw new Error("Not found") }) + const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) + const provider = makeProviderStub({ clineStack: [childTask] as any[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, updateTaskHistory, - // Seed the in-memory store with the interrupted child — mirrors what cancelTask - // writes before rehydrating, and is what removeClineFromStack now reads directly. - taskHistoryStore: { get: (id: string) => (id === childTaskId ? { status: "interrupted" } : undefined) }, + markDelegatedChildInterrupted, + taskHistoryStore: { + get: (id: string) => + id === childTaskId + ? { ...childHistoryItem } + : id === parentTaskId + ? { ...parentHistoryItem } + : undefined, + }, }) + // Simulate the navigation logic from createTaskWithHistoryItem: + // when the target is a delegated parent and current task is its interrupted child, + // removeClineFromStack must NOT repair parent to active. await (ClineProvider.prototype as any).removeClineFromStack.call(provider) - // Stack is emptied - expect(provider.clineStack).toHaveLength(0) + // Parent must stay delegated — no write at all + expect(updateTaskHistory).not.toHaveBeenCalledWith(expect.objectContaining({ id: parentTaskId })) + }) + + it("navigating away from an active delegated child marks the child interrupted", async () => { + // Option A: live eviction of an active delegated child → child becomes interrupted, + // parent stays delegated, user can resume or abandon later. + const childTaskId = "child-active" + const parentTaskId = "parent-1" + + const childHistoryItem = { + id: childTaskId, + status: "active", + parentTaskId, + } + + const parentHistoryItem = { + id: parentTaskId, + status: "delegated", + awaitingChildId: childTaskId, + delegatedToId: childTaskId, + } + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === parentTaskId) return { historyItem: { ...parentHistoryItem } } + if (id === childTaskId) return { historyItem: { ...childHistoryItem } } + throw new Error("Not found") + }) + + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId, + updateTaskHistory, + postMessageToWebview, + taskHistoryStore: { + get: (id: string) => + id === childTaskId + ? { ...childHistoryItem } + : id === parentTaskId + ? { ...parentHistoryItem } + : undefined, + }, + }) - // Parent must NOT be transitioned to active — it stays "delegated" - // so the interrupted child can resume and report back later + await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + childTaskId, + parentTaskId, + }) + + // Child becomes interrupted + expect(updateTaskHistory).toHaveBeenCalledWith( + expect.objectContaining({ id: childTaskId, status: "interrupted" }), + ) + // Parent stays delegated — awaitingChildId preserved expect(updateTaskHistory).not.toHaveBeenCalledWith( expect.objectContaining({ id: parentTaskId, status: "active" }), ) + expect(updateTaskHistory).not.toHaveBeenCalledWith( + expect.objectContaining({ id: parentTaskId, awaitingChildId: undefined }), + ) + }) +}) + +describe("ClineProvider.evictCurrentTask() — active delegated child path", () => { + it("calls markDelegatedChildInterrupted when current task is an active delegated child", async () => { + const childTaskId = "child-active" + const parentTaskId = "parent-1" + + const childTask = { + taskId: childTaskId, + instanceId: "inst-1", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + + const childHistoryItem = { id: childTaskId, status: "active", parentTaskId } + + const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + clineStack: [childTask] as any[], + taskEventListeners: new Map(), + getCurrentTask: vi.fn(() => childTask), + taskHistoryStore: { get: vi.fn((id: string) => (id === childTaskId ? childHistoryItem : undefined)) }, + markDelegatedChildInterrupted, + log: vi.fn(), + }) + + await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + + expect(provider.clineStack).toHaveLength(0) + expect(markDelegatedChildInterrupted).toHaveBeenCalledWith({ childTaskId, parentTaskId }) + }) + + it("does not call markDelegatedChildInterrupted when there is no current task", async () => { + const markDelegatedChildInterrupted = vi.fn() + + const provider = makeProviderStub({ + clineStack: [] as any[], + taskEventListeners: new Map(), + getCurrentTask: vi.fn(() => undefined), + taskHistoryStore: { get: vi.fn(() => undefined) }, + markDelegatedChildInterrupted, + log: vi.fn(), + }) + + await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + + expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() + }) + + it("does not call markDelegatedChildInterrupted for a task with no parentTaskId", async () => { + const childTask = { + taskId: "standalone-1", + instanceId: "inst-1", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + + const markDelegatedChildInterrupted = vi.fn() + + const provider = makeProviderStub({ + clineStack: [childTask] as any[], + taskEventListeners: new Map(), + getCurrentTask: vi.fn(() => childTask), + taskHistoryStore: { + get: vi.fn(() => ({ id: "standalone-1", status: "active", parentTaskId: undefined })), + }, + markDelegatedChildInterrupted, + log: vi.fn(), + }) + + await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + + expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() + }) + + it("propagates markDelegatedChildInterrupted errors (method swallows internally, not caller)", async () => { + // evictCurrentTask no longer has a caller-level .catch(); errors propagate + // from markDelegatedChildInterrupted directly. The real implementation swallows + // inside its own try/catch (after the guard reads); a mock that rejects bypasses + // that catch and exercises the propagation path. + const childTask = { + taskId: "child-err", + instanceId: "inst-1", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + + const markDelegatedChildInterrupted = vi.fn().mockRejectedValue(new Error("lock contention")) + + const provider = makeProviderStub({ + clineStack: [childTask] as any[], + taskEventListeners: new Map(), + getCurrentTask: vi.fn(() => childTask), + taskHistoryStore: { + get: vi.fn(() => ({ id: "child-err", status: "active", parentTaskId: "parent-1" })), + }, + markDelegatedChildInterrupted, + log: vi.fn(), + }) + + await expect((ClineProvider.prototype as any).evictCurrentTask.call(provider)).rejects.toThrow( + "lock contention", + ) + }) +}) + +describe("onTaskCompleted callback — writes completed status before re-emitting", () => { + function buildCallbackProvider(taskHistoryStoreGet: (id: string) => any) { + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const emit = vi.fn() + const log = vi.fn() + + // Wire up the real taskCreationCallback by calling the closure that ClineProvider + // sets on `this.taskCreationCallback` during construction. We extract it from the + // prototype's init code by calling the relevant portion directly. + const listeners: Record unknown)[]> = {} + const fakeTask = { + taskId: "task-1", + on: vi.fn((event: string, fn: (...args: unknown[]) => unknown) => { + listeners[event] = listeners[event] || [] + listeners[event].push(fn) + }), + emit: vi.fn((event: string, ...args: any[]) => { + listeners[event]?.forEach((fn) => fn(...args)) + }), + } + + const provider = makeProviderStub({ + taskHistoryStore: { get: taskHistoryStoreGet }, + updateTaskHistory, + emit, + log, + }) + + // Extract the real onTaskCompleted by simulating taskCreationCallback invocation. + // ClineProvider.prototype doesn't expose taskCreationCallback as a testable method, + // so we replicate the closure binding by calling the static block directly. + // The real callback is set in the constructor body; we replicate the relevant portion. + const onTaskCompleted = async (taskId: string) => { + try { + const existing = (provider as any).taskHistoryStore.get(taskId) + if (existing && existing.status !== "completed") { + await (provider as any).updateTaskHistory({ ...existing, status: "completed" }) + } + } catch (err) { + ;(provider as any).log( + `[onTaskCompleted] Failed to write completed status for ${taskId}: ${err instanceof Error ? err.message : String(err)}`, + ) + } + ;(provider as any).emit("TaskCompleted", taskId, {}, {}) + } + + return { onTaskCompleted, updateTaskHistory, emit, log } + } + + it("writes status:completed when existing record is not already completed", async () => { + const existingItem = { + id: "task-1", + status: "interrupted", + task: "T", + ts: 0, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const { onTaskCompleted, updateTaskHistory } = buildCallbackProvider((id) => + id === "task-1" ? existingItem : undefined, + ) + + await onTaskCompleted("task-1") + + expect(updateTaskHistory).toHaveBeenCalledWith(expect.objectContaining({ id: "task-1", status: "completed" })) + }) + + it("skips the write when existing record is already completed", async () => { + const existingItem = { id: "task-1", status: "completed" } + const { onTaskCompleted, updateTaskHistory } = buildCallbackProvider((id) => + id === "task-1" ? existingItem : undefined, + ) + + await onTaskCompleted("task-1") + + expect(updateTaskHistory).not.toHaveBeenCalled() + }) + + it("skips the write when taskHistoryStore has no entry for the task", async () => { + const { onTaskCompleted, updateTaskHistory } = buildCallbackProvider(() => undefined) + + await onTaskCompleted("task-1") + + expect(updateTaskHistory).not.toHaveBeenCalled() + }) + + it("logs and swallows errors from updateTaskHistory", async () => { + const existingItem = { id: "task-1", status: "active" } + const { onTaskCompleted, updateTaskHistory, log } = buildCallbackProvider((id) => + id === "task-1" ? existingItem : undefined, + ) + ;(updateTaskHistory as any).mockRejectedValue(new Error("disk full")) + + await expect(onTaskCompleted("task-1")).resolves.not.toThrow() + + expect(log).toHaveBeenCalledWith(expect.stringContaining("[onTaskCompleted] Failed to write")) }) }) diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index af5b429a80..29aab5d15d 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -39,9 +39,16 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) + const existingTask = { taskId: "existing-1" } const provider = { // Simulate an existing task present in stack - clineStack: [{ taskId: "existing-1" }], + clineStack: [existingTask], + getCurrentTask: vi.fn(() => existingTask), + taskHistoryStore: { get: vi.fn(() => undefined) }, + markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), + get evictCurrentTask() { + return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + }, setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, @@ -120,6 +127,11 @@ describe("Single-open-task invariant", () => { const provider = { getCurrentTask: vi.fn(() => undefined), // ensure not rehydrating + taskHistoryStore: { get: vi.fn(() => undefined) }, + markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), + get evictCurrentTask() { + return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + }, removeClineFromStack, addClineToStack, updateGlobalState, @@ -173,6 +185,12 @@ describe("Single-open-task invariant", () => { const createTask = vi.fn().mockResolvedValue({ taskId: "ipc-1" }) const provider = { context: {} as any, + getCurrentTask: vi.fn(() => undefined), + taskHistoryStore: { get: vi.fn(() => undefined) }, + markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), + get evictCurrentTask() { + return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + }, removeClineFromStack, postStateToWebview: vi.fn(), postMessageToWebview: vi.fn(), diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index 6e25f62eaf..fc43feae76 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -348,4 +348,22 @@ describe("registerCommands handlers", () => { `[toggleAutoApprove] postMessageToWebview failed: ${boom}`, ) }) + + it("plusButtonClicked calls evictCurrentTask on the visible provider", async () => { + const evictCurrentTask = vi.fn().mockResolvedValue(undefined) + const refreshWorkspace = vi.fn().mockResolvedValue(undefined) + ;(mockVisibleProvider as any).evictCurrentTask = evictCurrentTask + ;(mockVisibleProvider as any).refreshWorkspace = refreshWorkspace + + await handlers["zoo-code.plusButtonClicked"]() + + expect(evictCurrentTask).toHaveBeenCalledTimes(1) + }) + + it("plusButtonClicked is a no-op when no visible provider", async () => { + ;(ClineProvider.getVisibleInstance as Mock).mockReturnValue(undefined) + + // Should not throw even with no visible provider + await handlers["zoo-code.plusButtonClicked"]() + }) }) diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index db50edcee1..36b7c5d153 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -100,7 +100,7 @@ const getCommandsMap = ({ TelemetryService.instance.captureTitleButtonClicked("plus") - await visibleProvider.removeClineFromStack() + await visibleProvider.evictCurrentTask() await visibleProvider.refreshWorkspace() await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) // Send focusInput action immediately after chatButtonClicked diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 47e31d7e2d..613a5c1c1d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -167,14 +167,6 @@ export class ClineProvider private clineStack: Task[] = [] private delegationTransitionLocks?: Map> private cancelledDelegationChildIds = new Set() - // Marks a child whose cancellation is currently in flight, from the moment cancelTask() - // is invoked until its "interrupted" status write lands (or the cancel path bails out). - // removeClineFromStack()'s delegation repair must not run against a stale "active" read - // while this is set — otherwise a concurrent navigation (e.g. showTaskWithId(parentTaskId) - // from the user clicking "back to parent" right after hitting Stop) can win the race - // against cancelTask()'s own runDelegationTransition call and repair the parent to - // "active" before "interrupted" is ever persisted, permanently severing the delegation link. - private cancellingDelegationChildIds = new Set() private codeIndexStatusSubscription?: vscode.Disposable private codeIndexManager?: CodeIndexManager private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class @@ -288,7 +280,22 @@ export class ClineProvider // Create named listener functions so we can remove them later. const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId) - const onTaskCompleted = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) => { + const onTaskCompleted = async (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) => { + // Explicitly transition the task to "completed" so that any prior terminal + // status (e.g. "interrupted" from a previous cancel) is correctly overwritten. + // saveClineMessages() omits the status field for top-level tasks, which causes + // the store's merge to preserve a stale "interrupted" status after completion. + // interrupted → completed is a valid VALID_TRANSITIONS path. + try { + const existing = this.taskHistoryStore.get(taskId) + if (existing && existing.status !== "completed") { + await this.updateTaskHistory({ ...existing, status: "completed" }) + } + } catch (err) { + this.log( + `[onTaskCompleted] Failed to write completed status for ${taskId}: ${err instanceof Error ? err.message : String(err)}`, + ) + } this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage) } const onTaskAborted = async () => { @@ -488,9 +495,7 @@ export class ClineProvider // Removes and destroys the top Cline instance (the current finished task), // activating the previous one (resuming the parent task). - async removeClineFromStack(options?: { skipDelegationRepair?: boolean }) { - const callerStack = new Error().stack - + async removeClineFromStack() { if (this.clineStack.length === 0) { return } @@ -499,11 +504,6 @@ export class ClineProvider let task = this.clineStack.pop() if (task) { - // Capture delegation metadata before abort/dispose, since abortTask(true) - // is async and the task reference is cleared afterwards. - const childTaskId = task.taskId - const parentTaskId = task.parentTaskId - task.emit(RooCodeEventName.TaskUnfocused) try { @@ -527,66 +527,94 @@ export class ClineProvider // Make sure no reference kept, once promises end it will be // garbage collected. task = undefined + } + } - // Delegation-aware parent metadata repair: - // If the popped task was a delegated child, repair the parent's metadata - // so it transitions from "delegated" back to "active" and becomes resumable - // from the task history list. - // Skip when called from delegateParentAndOpenChild() during nested delegation - // transitions (A→B→C), where the caller intentionally replaces the active - // child and will update the parent to point at the new child. - if (parentTaskId && childTaskId && !options?.skipDelegationRepair) { - try { - await this.runDelegationTransition(parentTaskId, async () => { - const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId) - - if (parentHistory?.status === "delegated" && parentHistory?.awaitingChildId === childTaskId) { - // If the child is "interrupted", cancelTask already persisted that - // status and intentionally left the parent delegated so the child - // can resume and report back. Do not auto-repair in that case. - if (this.taskHistoryStore.get(childTaskId)?.status === "interrupted") { - this.log( - `[ClineProvider#removeClineFromStack] Skipping parent repair: child ${childTaskId} is interrupted`, - ) - return - } + /** + * Evicts the current task from the stack and, if it was an active delegated child, + * marks it interrupted so the parent stays delegated (rather than silently losing the link). + * + * Use this in place of bare removeClineFromStack() at any call site that is not itself + * part of a delegation transition (i.e. everywhere except delegateParentAndOpenChild, + * createTask with a parentTask, and reopenParentFromDelegation). + */ + public async evictCurrentTask(): Promise { + const current = this.getCurrentTask() + const storedHistory = current ? this.taskHistoryStore.get(current.taskId) : undefined + await this.removeClineFromStack() + if (storedHistory?.status === "active" && storedHistory.parentTaskId) { + await this.markDelegatedChildInterrupted({ + childTaskId: storedHistory.id, + parentTaskId: storedHistory.parentTaskId, + }) + } + } - // A cancellation for this child may be in flight (cancelTask() has - // marked it synchronously but its "interrupted" write hasn't landed - // yet, since both paths serialize on the same per-parent transition - // lock and this call won the race). Repairing here would clear - // awaitingChildId based on a stale "active" read and permanently - // sever the delegation link. Defer to cancelTask()'s own write instead. - if (this.cancellingDelegationChildIds.has(childTaskId)) { - this.log( - `[ClineProvider#removeClineFromStack] Skipping parent repair: cancellation for child ${childTaskId} is in flight`, - ) - return - } + /** + * Marks a live delegated child as "interrupted" when it is evicted without completing + * (e.g. user hits + for a new task, or navigates away while the child is still active). + * + * This preserves the delegation link — the parent stays "delegated" with awaitingChildId + * intact — so the user can later resume or abandon the interrupted child. It is the live- + * eviction counterpart to cancelTask()'s interruption path and to reopenParentFromDelegation() + * (which handles normal child completion). + * + * Must be called AFTER removeClineFromStack() so the live Task's final saveClineMessages() + * does not reattach the child's parentTaskId/rootTaskId over the interrupted status. + */ + private async markDelegatedChildInterrupted({ + childTaskId, + parentTaskId, + }: { + childTaskId: string + parentTaskId: string + }): Promise { + // Fast path: already interrupted (cancelTask beat us to it), nothing to do. + if (this.taskHistoryStore.get(childTaskId)?.status === "interrupted") { + this.log(`[markDelegatedChildInterrupted] Child ${childTaskId} already interrupted — skipping`) + return + } - assertValidTransition(parentHistory.status, "active") - await this.updateTaskHistory({ - ...parentHistory, - status: "active", - awaitingChildId: undefined, - delegatedToId: undefined, - }) - const repairMsg = - `[ClineProvider#removeClineFromStack] Repaired parent ${parentTaskId} metadata: delegated → active (child ${childTaskId} removed). ` + - `Caller stack: ${callerStack?.split("\n").slice(1, 5).join(" | ")}` - this.log(repairMsg) - console.warn(repairMsg) - } - }) - } catch (err) { - // Non-fatal: log but do not block the pop operation. + try { + await this.runDelegationTransition(parentTaskId, async () => { + const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId) + + if (parentHistory?.status !== "delegated" || parentHistory?.awaitingChildId !== childTaskId) { this.log( - `[ClineProvider#removeClineFromStack] Failed to repair parent metadata for ${parentTaskId} (non-fatal): ${ - err instanceof Error ? err.message : String(err) - }`, + `[markDelegatedChildInterrupted] Parent ${parentTaskId} no longer delegated to child ${childTaskId} — skipping`, ) + return } - } + + // Prefer the in-memory store entry: it is written by delegateParentAndOpenChild + // with the correct parentTaskId before the child saves its first message. + // getTaskWithId reads from disk and may return an incomplete record (missing + // parentTaskId) if the child was evicted before its first saveClineMessages(). + const childHistory = + this.taskHistoryStore.get(childTaskId) ?? (await this.getTaskWithId(childTaskId)).historyItem + + // Re-check inside the lock to close the TOCTOU window with cancelTask() or + // a concurrent completion. Only proceed when the child is still "active"; + // any other terminal status (interrupted, completed) must not be overwritten. + if (childHistory?.status !== "active") { + this.log( + `[markDelegatedChildInterrupted] Child ${childTaskId} is no longer active (status=${childHistory?.status}) — skipping`, + ) + return + } + + const interruptedChild = { ...childHistory, status: "interrupted" as const } + await this.updateTaskHistory(interruptedChild) + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: interruptedChild }) + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: parentHistory }) + this.log( + `[markDelegatedChildInterrupted] Marked child ${childTaskId} interrupted; parent ${parentTaskId} stays delegated`, + ) + }) + } catch (err) { + this.log( + `[markDelegatedChildInterrupted] Failed for child ${childTaskId}: ${err instanceof Error ? err.message : String(err)}`, + ) } } @@ -650,7 +678,12 @@ export class ClineProvider this._disposed = true this.log("Disposing ClineProvider...") - // Clear all tasks from the stack. + // Clear all tasks from the stack. The first pop goes through evictCurrentTask() + // so an active delegated child is marked interrupted before the extension shuts down, + // rather than being left persisted as "active" across the reload. + if (this.clineStack.length > 0) { + await this.evictCurrentTask() + } while (this.clineStack.length > 0) { await this.removeClineFromStack() } @@ -1021,7 +1054,7 @@ export class ClineProvider const isRehydratingCurrentTask = currentTask && currentTask.taskId === historyItem.id if (!isRehydratingCurrentTask) { - await this.removeClineFromStack() + await this.evictCurrentTask() } // If the history item has a saved mode, restore it and its associated API configuration. @@ -3107,13 +3140,11 @@ export class ClineProvider diffFuzzyThreshold, } = await this.getState() - // Single-open-task invariant: always enforce for user-initiated top-level tasks + // Single-open-task invariant: always enforce for user-initiated top-level tasks. if (!parentTask) { - try { - await this.removeClineFromStack() - } catch { + await this.evictCurrentTask().catch(() => { // Non-fatal - } + }) } if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) { @@ -3162,21 +3193,7 @@ export class ClineProvider } console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) - - // Mark this child as "cancellation in flight" synchronously, before any await, so a - // concurrent removeClineFromStack() (e.g. from the user navigating back to the parent - // right after clicking Stop) cannot win the race against this function's own - // runDelegationTransition call below and repair the parent from a stale "active" read - // before "interrupted" is persisted (see cancellingDelegationChildIds doc comment). - if (task.parentTaskId) { - this.cancellingDelegationChildIds.add(task.taskId) - } - - try { - await this.cancelTaskInternal(task) - } finally { - this.cancellingDelegationChildIds.delete(task.taskId) - } + await this.cancelTaskInternal(task) } private async cancelTaskInternal(task: Task): Promise { @@ -3532,7 +3549,7 @@ export class ClineProvider // This ensures we never have >1 tasks open at any time during delegation. // Await abort completion to ensure clean disposal and prevent unhandled rejections. try { - await this.removeClineFromStack({ skipDelegationRepair: true }) + await this.removeClineFromStack() } catch (error) { this.log( `[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${ @@ -3578,13 +3595,44 @@ export class ClineProvider // single lock acquisition — no concurrent writer can slip between the read and // write, and the pure updater cannot re-enter the lock (no deadlock). // Broadcast and cache invalidation happen outside the lock after it releases. + // + // If the parent is already "delegated" to a previous interrupted child (the user + // navigated back to the parent and continued working), we implicitly sever the old + // link here (delegated → active → delegated) so no explicit Abandon step is needed. + // The old awaited child's status is re-read INSIDE the updater (which runs + // synchronously under the store lock) so a concurrent abandon or completion cannot + // slip between the status snapshot and the write. An active child must never be + // silently detached. try { await this.taskHistoryStore.atomicReadAndUpdate(parentTaskId, (historyItem) => { - assertValidTransition(historyItem.status, "delegated") - const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId])) + let base = historyItem + if (historyItem.status === "delegated") { + // Re-read the awaited child's current status under the store lock. + const awaitedChildStatus = historyItem.awaitingChildId + ? this.taskHistoryStore.get(historyItem.awaitingChildId)?.status + : undefined + // Only sever the stale link when the old child is confirmed interrupted. + // If it is still active, throw so the rollback path cleans up the new child + // rather than silently detaching a live task. + if (awaitedChildStatus !== "interrupted") { + throw new Error( + `[delegateParentAndOpenChild] Cannot re-delegate: existing child ${historyItem.awaitingChildId} is ${awaitedChildStatus}, not interrupted`, + ) + } + // Implicit sever of the stale interrupted-child link. + // The old child keeps its interrupted status; we just clear the parent's pointer. + base = { + ...historyItem, + status: "active" as const, + awaitingChildId: undefined, + delegatedToId: undefined, + } + } + assertValidTransition(base.status, "delegated") + const childIds = Array.from(new Set([...(base.childIds ?? []), child.taskId])) return { - ...historyItem, - status: "delegated", + ...base, + status: "delegated" as const, delegatedToId: child.taskId, awaitingChildId: child.taskId, childIds, @@ -3607,7 +3655,7 @@ export class ClineProvider // Only pop the stack if the child we just created is still on top. // A concurrent delegation could have pushed another child since we created ours. if (this.getCurrentTask()?.taskId === child.taskId) { - await this.removeClineFromStack({ skipDelegationRepair: true }) + await this.removeClineFromStack() } } catch (cleanupError) { this.log( @@ -3818,7 +3866,7 @@ export class ClineProvider // overwrite a "completed" status set later. const current = this.getCurrentTask() if (current?.taskId === childTaskId) { - await this.removeClineFromStack({ skipDelegationRepair: true }) + await this.removeClineFromStack() } // 3+5) Atomically mark child completed and parent active in one lock acquisition. @@ -3905,6 +3953,106 @@ export class ClineProvider }) } + /** + * Explicitly sever a delegated parent-child link, e.g. when the user gives up on + * an "interrupted" subtask instead of resuming it. Unlike removeClineFromStack()'s + * automatic repair, this is user-initiated and works even while the child is + * "interrupted" (which removeClineFromStack intentionally leaves alone so the child + * can still resume and report back). Only interrupted children can be abandoned — a + * still-running child must be cancelled first, so its link is never severed mid-stream. + * + * Parent transitions delegated → active (its normal "no longer awaiting a child" + * state). The child's own status is left untouched (interrupted stays interrupted; + * VALID_TRANSITIONS only allows interrupted → completed) — only its parent/root + * links are cleared so a later resume-and-complete cannot reattach it. + */ + public async abandonSubtask(childTaskId: string): Promise { + const { historyItem: childHistory } = await this.getTaskWithId(childTaskId) + const parentTaskId = childHistory.parentTaskId + + if (!parentTaskId) { + return false + } + + // Only an interrupted (cancelled, not running) child may be abandoned. A still-running + // child must be cancelled first — severing the link out from under a live stream would + // orphan it silently instead of giving the user the normal cancel/resume flow. + if (childHistory.status !== "interrupted") { + this.log( + `[abandonSubtask] Aborting: child ${childTaskId} is not interrupted (status=${childHistory.status})`, + ) + return false + } + + return this.runDelegationTransition(parentTaskId, async () => { + const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId) + + if (parentHistory?.status !== "delegated" || parentHistory?.awaitingChildId !== childTaskId) { + this.log( + `[abandonSubtask] Aborting: parent ${parentTaskId} is no longer delegated to child ${childTaskId} ` + + `(status=${parentHistory?.status}, awaitingChildId=${parentHistory?.awaitingChildId})`, + ) + return false + } + + // Re-check inside the lock: the child may have been resumed (and be streaming again, + // or have completed) between the check above and acquiring the delegation transition lock. + const freshChild = this.taskHistoryStore.get(childTaskId) + if (freshChild?.status !== "interrupted") { + this.log( + `[abandonSubtask] Aborting: child ${childTaskId} is no longer interrupted (status=${freshChild?.status})`, + ) + return false + } + + assertValidTransition(parentHistory.status, "active") + + // Close the live child instance (if it's still the open task — the common case, + // since an interrupted child is rehydrated onto the stack after cancelTask) BEFORE + // clearing its persisted links. Task#saveClineMessages() rebuilds parentTaskId/ + // rootTaskId from the live (readonly) Task fields on every save, so any save that + // happens after we clear the persisted links — including abortTask's own final + // save — would silently reattach the child to its old parent. + const current = this.getCurrentTask() + if (current?.taskId === childTaskId) { + await this.removeClineFromStack() + } + + await this.taskHistoryStore.atomicUpdatePair( + childTaskId, + parentTaskId, + (child) => ({ ...child, parentTaskId: undefined, rootTaskId: undefined }), + (parent) => ({ + ...parent, + status: "active" as const, + awaitingChildId: undefined, + delegatedToId: undefined, + }), + ) + this.recentTasksCache = undefined + + // Guard against a stale in-flight resume/completion (e.g. a resume that was already + // in progress when abandon was clicked) reattaching the child after the link above + // was cleared. AttemptCompletionTool re-reads parent status from the persisted store, + // not the live task's readonly parentTaskId field, so this is the authoritative gate. + this.cancelledDelegationChildIds.add(childTaskId) + + if (this.isViewLaunched) { + const updatedChild = this.taskHistoryStore.get(childTaskId) + const updatedParent = this.taskHistoryStore.get(parentTaskId) + if (updatedChild) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedChild }) + } + if (updatedParent) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedParent }) + } + } + + this.log(`[abandonSubtask] Severed link between parent ${parentTaskId} and child ${childTaskId}`) + return true + }) + } + /** * Convert a file path to a webview-accessible URI * This method safely converts file paths to URIs that can be loaded in the webview diff --git a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts index 555253c345..37aae2ddb7 100644 --- a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts @@ -780,75 +780,9 @@ describe("ClineProvider flicker-free cancel", () => { ) }) - it("removeClineFromStack does not repair parent when child is interrupted", async () => { - const parentHistory: HistoryItem = { - id: "parent-1", - number: 1, - task: "parent task", - ts: Date.now(), - tokensIn: 10, - tokensOut: 20, - totalCost: 0.001, - workspace: "/test/workspace", - status: "delegated", - awaitingChildId: "child-1", - delegatedToId: "child-1", - } - - const childTask = { - taskId: "child-1", - instanceId: "inst-child", - parentTaskId: "parent-1", - emit: vi.fn(), - abortTask: vi.fn().mockResolvedValue(undefined), - } - ;(provider as any).clineStack = [childTask] - ;(provider as any).taskEventListeners = new Map() - // Seed the in-memory store so taskHistoryStore.get("child-1") returns interrupted - vi.spyOn((provider as any).taskHistoryStore, "get").mockImplementation((id: unknown) => - id === "child-1" ? { status: "interrupted" } : undefined, - ) - - provider.getTaskWithId = vi.fn().mockImplementation((id) => { - if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) - throw new Error(`unexpected task lookup: ${id}`) - }) as any - - const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) - - await (provider as any).removeClineFromStack() - - // Parent must NOT be transitioned to active — it stays delegated - expect(updateTaskHistorySpy).not.toHaveBeenCalledWith( - expect.objectContaining({ id: "parent-1", status: "active" }), - ) - }) - - // Regression test for the race where a user clicks Stop on a freshly-delegated - // child and immediately navigates back to the parent (showTaskWithId), before - // cancelTask()'s own persistence of childHistory.status = "interrupted" has - // landed. Both cancelTask() and removeClineFromStack() serialize their parent - // writes through runDelegationTransition(parentTaskId, ...), but removeClineFromStack - // only skips its repair when taskHistoryStore.get(childTaskId)?.status === "interrupted". - // If removeClineFromStack's transition wins the race and runs while the store still - // reports "active" (the write from cancelTask() hasn't landed yet), it incorrectly - // repairs the parent to "active" and clears awaitingChildId, permanently severing - // the delegation link before the child ever gets a chance to report back. - it("removeClineFromStack does not repair parent when a cancellation for the child is in flight", async () => { - const parentHistory: HistoryItem = { - id: "parent-1", - number: 1, - task: "parent task", - ts: Date.now(), - tokensIn: 10, - tokensOut: 20, - totalCost: 0.001, - workspace: "/test/workspace", - status: "delegated", - awaitingChildId: "child-1", - delegatedToId: "child-1", - } - + it("removeClineFromStack never mutates delegation metadata (pure lifecycle after refactor)", async () => { + // After the refactor, removeClineFromStack() is pure lifecycle: pop, abort, clean up. + // Delegation state is owned by reopenParentFromDelegation() and markDelegatedChildInterrupted(). const childTask = { taskId: "child-1", instanceId: "inst-child", @@ -859,31 +793,16 @@ describe("ClineProvider flicker-free cancel", () => { ;(provider as any).clineStack = [childTask] ;(provider as any).taskEventListeners = new Map() - // The store still reports "active" — cancelTask()'s write to "interrupted" - // has not landed yet. This is the pre-write window of the race. - vi.spyOn((provider as any).taskHistoryStore, "get").mockImplementation((id: unknown) => - id === "child-1" ? { status: "active" } : undefined, - ) - - provider.getTaskWithId = vi.fn().mockImplementation((id) => { - if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) - throw new Error(`unexpected task lookup: ${id}`) - }) as any - + provider.getTaskWithId = vi.fn() as any const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) - // Simulate cancelTask() having already synchronously marked this child as - // "being cancelled" before its own await chain reaches the history write. - ;(provider as any).cancellingDelegationChildIds.add("child-1") - await (provider as any).removeClineFromStack() - // Parent must NOT be transitioned to active while the child's cancellation - // is still in flight — repairing here would clear awaitingChildId and - // permanently sever the delegation link before "interrupted" is persisted. - expect(updateTaskHistorySpy).not.toHaveBeenCalledWith( - expect.objectContaining({ id: "parent-1", status: "active" }), - ) + expect((provider as any).clineStack).toHaveLength(0) + expect(childTask.abortTask).toHaveBeenCalledWith(true) + // No history writes — lifecycle only + expect(updateTaskHistorySpy).not.toHaveBeenCalled() + expect(provider.getTaskWithId).not.toHaveBeenCalled() }) afterAll(() => { diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index 4a76ab4858..fe1eac8e20 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode" import type { HistoryItem, ExtensionMessage } from "@roo-code/types" +import { RooCodeEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { ContextProxy } from "../../config/ContextProxy" @@ -780,4 +781,70 @@ describe("ClineProvider Task History Synchronization", () => { expect(item!.tokensIn).toBe(222) }) }) + + describe("taskCreationCallback — onTaskCompleted listener", () => { + function makeFakeTask(taskId: string) { + const listeners: Record unknown)[]> = {} + return { + taskId, + on: (event: string, fn: (...args: unknown[]) => unknown) => { + listeners[event] = listeners[event] ?? [] + listeners[event].push(fn) + }, + // Returns a promise that resolves when all async listeners have settled. + emit: async (event: string, ...args: unknown[]) => { + await Promise.all((listeners[event] ?? []).map((fn) => Promise.resolve(fn(...args)))) + }, + } + } + + it("writes completed status when task is not already completed", async () => { + const existing = createHistoryItem({ id: "task-cb-1", task: "T" }) + await provider.updateTaskHistory(existing, { broadcast: false }) + + const fakeTask = makeFakeTask("task-cb-1") + ;(provider as any).taskCreationCallback(fakeTask) + + await fakeTask.emit(RooCodeEventName.TaskCompleted, "task-cb-1", {}, {}) + + const stored = provider.taskHistoryStore.get("task-cb-1") + expect(stored?.status).toBe("completed") + }) + + it("skips the write when task is already completed", async () => { + const existing = createHistoryItem({ id: "task-cb-2", task: "T", status: "completed" }) + await provider.updateTaskHistory(existing, { broadcast: false }) + + const updateSpy = vi.spyOn(provider, "updateTaskHistory") + + const fakeTask = makeFakeTask("task-cb-2") + ;(provider as any).taskCreationCallback(fakeTask) + + await fakeTask.emit(RooCodeEventName.TaskCompleted, "task-cb-2", {}, {}) + + // updateTaskHistory is called initially to store the item, but should NOT be + // called again by onTaskCompleted since it's already completed. + const onTaskCompletedCalls = updateSpy.mock.calls.filter((c) => { + const item = c[0] as HistoryItem + return item?.id === "task-cb-2" && item?.status === "completed" + }) + // It was written with completed status already; the callback must not re-write. + expect(onTaskCompletedCalls.length).toBe(0) + }) + + it("logs and does not throw when updateTaskHistory rejects", async () => { + const existing = createHistoryItem({ id: "task-cb-3", task: "T" }) + await provider.updateTaskHistory(existing, { broadcast: false }) + + vi.spyOn(provider, "updateTaskHistory").mockRejectedValueOnce(new Error("disk full")) + const logSpy = vi.spyOn(provider as any, "log") + + const fakeTask = makeFakeTask("task-cb-3") + ;(provider as any).taskCreationCallback(fakeTask) + + await fakeTask.emit(RooCodeEventName.TaskCompleted, "task-cb-3", {}, {}) + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[onTaskCompleted] Failed to write")) + }) + }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts new file mode 100644 index 0000000000..c1b6cf4dcf --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts @@ -0,0 +1,53 @@ +// npx vitest run src/core/webview/__tests__/webviewMessageHandler.abandonSubtask.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" + +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => key), + changeLanguage: vi.fn(), +})) + +vi.mock("vscode", () => ({ + window: { showErrorMessage: vi.fn() }, + workspace: { workspaceFolders: undefined }, +})) + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +describe("webviewMessageHandler — abandonSubtaskWithId", () => { + let provider: { abandonSubtask: ReturnType; log: (msg: string) => void } + + beforeEach(() => { + vi.clearAllMocks() + provider = { + abandonSubtask: vi.fn().mockResolvedValue(true), + log: vi.fn(), + } + }) + + it("calls provider.abandonSubtask with the message text", async () => { + await webviewMessageHandler(provider as unknown as ClineProvider, { + type: "abandonSubtaskWithId", + text: "child-task-99", + }) + + expect(provider.abandonSubtask).toHaveBeenCalledWith("child-task-99") + }) + + it("catches and logs errors from provider.abandonSubtask", async () => { + const err = new Error("sever failed") + provider.abandonSubtask!.mockRejectedValue(err) + + // webviewMessageHandler calls .catch() on the promise — it should not throw + await webviewMessageHandler(provider as unknown as ClineProvider, { + type: "abandonSubtaskWithId", + text: "child-task-99", + }) + + // Give the microtask queue a tick so the .catch() fires + await new Promise((r) => setTimeout(r, 0)) + + expect(provider.log).toHaveBeenCalledWith(expect.stringContaining("sever failed")) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 35bec4e8bb..dd15879c1c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -833,6 +833,15 @@ export const webviewMessageHandler = async ( case "deleteTaskWithId": provider.deleteTaskWithId(message.text!) break + case "abandonSubtaskWithId": + provider + .abandonSubtask(message.text!) + .catch((error) => + provider.log( + `[abandonSubtaskWithId] Failed: ${error instanceof Error ? error.message : String(error)}`, + ), + ) + break case "deleteMultipleTasksWithIds": { const ids = message.ids diff --git a/src/extension/api.ts b/src/extension/api.ts index c140ec8ec0..2d0d5a6975 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -192,7 +192,7 @@ export class API extends EventEmitter implements RooCodeAPI { provider = this.sidebarProvider } - await provider.removeClineFromStack() + await provider.evictCurrentTask() await provider.postStateToWebview() await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images }) @@ -255,7 +255,7 @@ export class API extends EventEmitter implements RooCodeAPI { public async clearCurrentTask(_lastMessage?: string) { // Legacy finishSubTask removed; clear current by closing active task instance. - await this.sidebarProvider.removeClineFromStack() + await this.sidebarProvider.evictCurrentTask() await this.sidebarProvider.postStateToWebview() } @@ -263,6 +263,10 @@ export class API extends EventEmitter implements RooCodeAPI { await this.sidebarProvider.cancelTask() } + public async abandonSubtask(childTaskId: string): Promise { + return this.sidebarProvider.abandonSubtask(childTaskId) + } + public async sendMessage(text?: string, images?: string[]) { const currentTask = this.sidebarProvider.getCurrentTask() diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 8687c8e3fa..0941a22e2b 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -1,6 +1,14 @@ import { memo, useRef, useState, useMemo } from "react" import { useTranslation } from "react-i18next" -import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, ListChevronsDownUp, ArrowLeft } from "lucide-react" +import { + ChevronUp, + ChevronDown, + HardDriveDownload, + HardDriveUpload, + ListChevronsDownUp, + ArrowLeft, + ArrowRight, +} from "lucide-react" import prettyBytes from "pretty-bytes" import type { ClineMessage } from "@roo-code/types" @@ -15,7 +23,6 @@ import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { vscode } from "@src/utils/vscode" import Thumbnails from "../common/Thumbnails" - import { TaskActions } from "./TaskActions" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" @@ -99,6 +106,15 @@ const TaskHeader = ({ } } + // Is this task itself a delegated parent still waiting on a subtask? See #559. + const awaitingChildId = currentTaskItem?.status === "delegated" ? currentTaskItem.awaitingChildId : undefined + + const handleGoToSubtask = () => { + if (awaitingChildId) { + vscode.postMessage({ type: "showTaskWithId", text: awaitingChildId }) + } + } + return (
{isSubtask && ( @@ -113,6 +129,19 @@ const TaskHeader = ({
)} + {awaitingChildId && ( +
e.stopPropagation()}> + +
+ )}
{ }) }) + describe("Delegated parent waiting-on-subtask banner", () => { + beforeEach(() => { + mockPostMessage.mockClear() + }) + + afterEach(() => { + mockExtensionState.currentTaskItem = { id: "test-task-id" } + mockExtensionState.taskHistory = [] + }) + + it("does not show the banner when currentTaskItem is not delegated", () => { + mockExtensionState.currentTaskItem = { id: "test-task-id", status: "active" } as any + renderTaskHeader() + expect(screen.queryByText("chat:task.waitingOnSubtask")).not.toBeInTheDocument() + }) + + it("shows the banner when currentTaskItem is delegated with an awaitingChildId", () => { + mockExtensionState.currentTaskItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + } as any + renderTaskHeader() + expect(screen.getByText("chat:task.waitingOnSubtask")).toBeInTheDocument() + }) + + it("navigates to the awaited child when the banner is clicked", () => { + mockExtensionState.currentTaskItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + } as any + renderTaskHeader() + + fireEvent.click(screen.getByText("chat:task.waitingOnSubtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ type: "showTaskWithId", text: "child-1" }) + }) + + it("does not show an Abandon button (removed: implicit sever on re-delegation)", () => { + mockExtensionState.currentTaskItem = { + id: "parent-1", + status: "delegated", + awaitingChildId: "child-1", + } as any + renderTaskHeader() + + expect(screen.getByText("chat:task.waitingOnSubtask")).toBeInTheDocument() + expect(screen.queryByText("history:abandonSubtask")).not.toBeInTheDocument() + }) + }) + describe("Context window percentage calculation", () => { // The percentage should be calculated as: // contextTokens / (contextWindow - reservedForOutput) * 100 diff --git a/webview-ui/src/components/history/SubtaskRow.tsx b/webview-ui/src/components/history/SubtaskRow.tsx index 0089e1f81d..c0aa88489a 100644 --- a/webview-ui/src/components/history/SubtaskRow.tsx +++ b/webview-ui/src/components/history/SubtaskRow.tsx @@ -6,6 +6,7 @@ import type { SubtaskTreeNode } from "./types" import { countAllSubtasks } from "./types" import { StandardTooltip } from "../ui" import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow" +import { TaskStatusBadge } from "./TaskStatusBadge" interface SubtaskRowProps { /** The subtask tree node to display */ @@ -52,6 +53,9 @@ const SubtaskRow = ({ node, depth, onToggleExpand, className }: SubtaskRowProps) {item.task} + {(item.status === "delegated" || item.status === "interrupted") && ( + + )}
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index d0dc367e64..72c6b64420 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -7,6 +7,7 @@ import { DeleteButton } from "./DeleteButton" import { StandardTooltip } from "../ui/standard-tooltip" import { useAppTranslation } from "@/i18n/TranslationContext" import { Split } from "lucide-react" +import { TaskStatusBadge } from "./TaskStatusBadge" export interface TaskItemFooterProps { item: HistoryItem @@ -36,6 +37,13 @@ const TaskItemFooter: React.FC = ({ · )} + {/* Delegation status (delegated parent waiting on a child, or interrupted child) */} + {(item.status === "delegated" || item.status === "interrupted") && ( + <> + + · + + )} {/* Datetime with time-ago format */} {formatTimeAgo(item.ts)} diff --git a/webview-ui/src/components/history/TaskStatusBadge.tsx b/webview-ui/src/components/history/TaskStatusBadge.tsx new file mode 100644 index 0000000000..5278f1422e --- /dev/null +++ b/webview-ui/src/components/history/TaskStatusBadge.tsx @@ -0,0 +1,36 @@ +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@/lib/utils" +import { StandardTooltip } from "../ui" + +interface TaskStatusBadgeProps { + status: "delegated" | "interrupted" + className?: string +} + +/** + * Small inline badge for a task's delegation status: a parent "delegated" and + * waiting on a subtask, or a child that was "interrupted" mid-execution and + * can be resumed rather than silently detached. See #559. + */ +export const TaskStatusBadge = ({ status, className }: TaskStatusBadgeProps) => { + const { t } = useAppTranslation() + + const isInterrupted = status === "interrupted" + const icon = isInterrupted ? "codicon-warning" : "codicon-sync" + const label = isInterrupted ? t("history:interruptedTag") : t("history:delegatedTag") + + return ( + + + + {label} + + + ) +} diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index aa334d94c2..fc8d7edcca 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -94,4 +94,24 @@ describe("TaskItemFooter", () => { expect(screen.queryByText("history:subtaskTag")).not.toBeInTheDocument() }) + + it("shows a delegated status badge when status is delegated", () => { + render( + , + ) + + expect(screen.getByTestId("task-status-badge-delegated")).toBeInTheDocument() + }) + + it("shows an interrupted status badge when status is interrupted", () => { + render() + + expect(screen.getByTestId("task-status-badge-interrupted")).toBeInTheDocument() + }) + + it("does not show a status badge for a completed task", () => { + render() + + expect(screen.queryByTestId(/task-status-badge-/)).not.toBeInTheDocument() + }) }) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 6e78f21ef2..fe301eb4cc 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Condensar context de forma intel·ligent", "openApiHistory": "Obrir historial d'API", "openUiHistory": "Obrir historial d'UI", - "backToParentTask": "Tasca principal" + "backToParentTask": "Tasca principal", + "waitingOnSubtask": "Esperant subtasca", + "goToSubtask": "Anar a la subtasca" }, "unpin": "Desfixar", "pin": "Fixar", diff --git a/webview-ui/src/i18n/locales/ca/history.json b/webview-ui/src/i18n/locales/ca/history.json index ab1bba7582..a872651d23 100644 --- a/webview-ui/src/i18n/locales/ca/history.json +++ b/webview-ui/src/i18n/locales/ca/history.json @@ -54,5 +54,7 @@ "subtaskTag": "Subtasca", "deleteWithSubtasks": "Això també eliminarà {{count}} subtasca(s). Estàs segur?", "expandSubtasks": "Expandir subtasques", - "collapseSubtasks": "Contreure subtasques" + "collapseSubtasks": "Contreure subtasques", + "delegatedTag": "Esperant subtasca", + "interruptedTag": "Interrompuda" } diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index a9d3472229..a2cc52b716 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Kontext intelligent komprimieren", "openApiHistory": "API-Verlauf öffnen", "openUiHistory": "UI-Verlauf öffnen", - "backToParentTask": "Übergeordnete Aufgabe" + "backToParentTask": "Übergeordnete Aufgabe", + "waitingOnSubtask": "Wartet auf Unteraufgabe", + "goToSubtask": "Zur Unteraufgabe" }, "unpin": "Lösen von oben", "pin": "Anheften", diff --git a/webview-ui/src/i18n/locales/de/history.json b/webview-ui/src/i18n/locales/de/history.json index 46064d6e2e..b10fcd445e 100644 --- a/webview-ui/src/i18n/locales/de/history.json +++ b/webview-ui/src/i18n/locales/de/history.json @@ -54,5 +54,7 @@ "subtaskTag": "Teilaufgabe", "deleteWithSubtasks": "Dies löscht auch {{count}} Teilaufgabe(n). Bist du sicher?", "expandSubtasks": "Teilaufgaben erweitern", - "collapseSubtasks": "Teilaufgaben einklappen" + "collapseSubtasks": "Teilaufgaben einklappen", + "delegatedTag": "Wartet auf Unteraufgabe", + "interruptedTag": "Unterbrochen" } diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index f3e2d85565..00438c3bfc 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -17,7 +17,9 @@ "delete": "Delete Task (Shift + Click to skip confirmation)", "openApiHistory": "Open API History", "openUiHistory": "Open UI History", - "backToParentTask": "Parent task" + "backToParentTask": "Parent task", + "waitingOnSubtask": "Waiting on subtask", + "goToSubtask": "Go to subtask" }, "unpin": "Unpin", "pin": "Pin", diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json index 85174890e1..6d53cd3663 100644 --- a/webview-ui/src/i18n/locales/en/history.json +++ b/webview-ui/src/i18n/locales/en/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Subtask", "deleteWithSubtasks": "This will also delete {{count}} subtask(s). Are you sure?", "expandSubtasks": "Expand subtasks", - "collapseSubtasks": "Collapse subtasks" + "collapseSubtasks": "Collapse subtasks", + "delegatedTag": "Waiting on subtask", + "interruptedTag": "Interrupted" } diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 9111e34430..d690b09cf2 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Condensar contexto de forma inteligente", "openApiHistory": "Abrir historial de API", "openUiHistory": "Abrir historial de UI", - "backToParentTask": "Tarea principal" + "backToParentTask": "Tarea principal", + "waitingOnSubtask": "Esperando subtarea", + "goToSubtask": "Ir a la subtarea" }, "unpin": "Desfijar", "pin": "Fijar", diff --git a/webview-ui/src/i18n/locales/es/history.json b/webview-ui/src/i18n/locales/es/history.json index 820f003d40..91d28abb99 100644 --- a/webview-ui/src/i18n/locales/es/history.json +++ b/webview-ui/src/i18n/locales/es/history.json @@ -54,5 +54,7 @@ "subtaskTag": "Subtarea", "deleteWithSubtasks": "Esto también eliminará {{count}} subtarea(s). ¿Estás seguro?", "expandSubtasks": "Expandir subtareas", - "collapseSubtasks": "Contraer subtareas" + "collapseSubtasks": "Contraer subtareas", + "delegatedTag": "Esperando subtarea", + "interruptedTag": "Interrumpida" } diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 3238fcb101..12daca0d26 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Condenser intelligemment le contexte", "openApiHistory": "Ouvrir l'historique de l'API", "openUiHistory": "Ouvrir l'historique de l'UI", - "backToParentTask": "Tâche parente" + "backToParentTask": "Tâche parente", + "waitingOnSubtask": "En attente de sous-tâche", + "goToSubtask": "Aller à la sous-tâche" }, "unpin": "Désépingler", "pin": "Épingler", diff --git a/webview-ui/src/i18n/locales/fr/history.json b/webview-ui/src/i18n/locales/fr/history.json index d84cfcb190..443bb0eb3e 100644 --- a/webview-ui/src/i18n/locales/fr/history.json +++ b/webview-ui/src/i18n/locales/fr/history.json @@ -54,5 +54,7 @@ "subtaskTag": "Sous-tâche", "deleteWithSubtasks": "Cela supprimera aussi {{count}} sous-tâche(s). Êtes-vous sûr ?", "expandSubtasks": "Développer les sous-tâches", - "collapseSubtasks": "Réduire les sous-tâches" + "collapseSubtasks": "Réduire les sous-tâches", + "delegatedTag": "En attente de sous-tâche", + "interruptedTag": "Interrompue" } diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index a1a0ed56a2..167802f730 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -17,7 +17,9 @@ "condenseContext": "संदर्भ को बुद्धिमानी से संघनित करें", "openApiHistory": "API इतिहास खोलें", "openUiHistory": "UI इतिहास खोलें", - "backToParentTask": "मूल कार्य" + "backToParentTask": "मूल कार्य", + "waitingOnSubtask": "उपकार्य की प्रतीक्षा", + "goToSubtask": "उपकार्य पर जाएं" }, "unpin": "पिन करें", "pin": "अवपिन करें", diff --git a/webview-ui/src/i18n/locales/hi/history.json b/webview-ui/src/i18n/locales/hi/history.json index 0d4cb40dd9..3dd7cca9a9 100644 --- a/webview-ui/src/i18n/locales/hi/history.json +++ b/webview-ui/src/i18n/locales/hi/history.json @@ -47,5 +47,7 @@ "subtaskTag": "उप-कार्य", "deleteWithSubtasks": "यह {{count}} उप-कार्य(कों) को भी हटा देगा। क्या आप निश्चित हैं?", "expandSubtasks": "उप-कार्य विस्तारित करें", - "collapseSubtasks": "उप-कार्य संपीड़ित करें" + "collapseSubtasks": "उप-कार्य संपीड़ित करें", + "delegatedTag": "उपकार्य की प्रतीक्षा", + "interruptedTag": "बाधित" } diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index f0e73462e9..c6f38c7d0e 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -17,7 +17,9 @@ "delete": "Hapus Tugas (Shift + Klik untuk lewati konfirmasi)", "openApiHistory": "Buka Riwayat API", "openUiHistory": "Buka Riwayat UI", - "backToParentTask": "Tugas Induk" + "backToParentTask": "Tugas Induk", + "waitingOnSubtask": "Menunggu subtugas", + "goToSubtask": "Pergi ke subtugas" }, "history": { "title": "Riwayat" diff --git a/webview-ui/src/i18n/locales/id/history.json b/webview-ui/src/i18n/locales/id/history.json index 7796061107..772ca25384 100644 --- a/webview-ui/src/i18n/locales/id/history.json +++ b/webview-ui/src/i18n/locales/id/history.json @@ -56,5 +56,7 @@ "subtaskTag": "Subtask", "deleteWithSubtasks": "Ini juga akan menghapus {{count}} subtask. Apakah Anda yakin?", "expandSubtasks": "Perluas subtask", - "collapseSubtasks": "Tutup subtask" + "collapseSubtasks": "Tutup subtask", + "delegatedTag": "Menunggu subtugas", + "interruptedTag": "Terganggu" } diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 2056e8d977..79d61965c7 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Condensa contesto in modo intelligente", "openApiHistory": "Apri cronologia API", "openUiHistory": "Apri cronologia UI", - "backToParentTask": "Attività principale" + "backToParentTask": "Attività principale", + "waitingOnSubtask": "In attesa di sottoattività", + "goToSubtask": "Vai alla sottoattività" }, "unpin": "Rilascia", "pin": "Fissa", diff --git a/webview-ui/src/i18n/locales/it/history.json b/webview-ui/src/i18n/locales/it/history.json index aa728ef8f6..4097d43ce2 100644 --- a/webview-ui/src/i18n/locales/it/history.json +++ b/webview-ui/src/i18n/locales/it/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Sottoattività", "deleteWithSubtasks": "Questo eliminerà anche {{count}} sottoattività. Sei sicuro?", "expandSubtasks": "Espandi sottoattività", - "collapseSubtasks": "Comprimi sottoattività" + "collapseSubtasks": "Comprimi sottoattività", + "delegatedTag": "In attesa di sottoattività", + "interruptedTag": "Interrotta" } diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 88aa788626..04e7b13691 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -17,7 +17,9 @@ "condenseContext": "コンテキストをインテリジェントに圧縮", "openApiHistory": "API履歴を開く", "openUiHistory": "UI履歴を開く", - "backToParentTask": "親タスク" + "backToParentTask": "親タスク", + "waitingOnSubtask": "サブタスク待ち", + "goToSubtask": "サブタスクへ" }, "unpin": "ピン留めを解除", "pin": "ピン留め", diff --git a/webview-ui/src/i18n/locales/ja/history.json b/webview-ui/src/i18n/locales/ja/history.json index b73baa3a76..be4897ba34 100644 --- a/webview-ui/src/i18n/locales/ja/history.json +++ b/webview-ui/src/i18n/locales/ja/history.json @@ -47,5 +47,7 @@ "subtaskTag": "サブタスク", "deleteWithSubtasks": "これにより {{count}} サブタスクも削除されます。よろしいですか?", "expandSubtasks": "サブタスクを展開", - "collapseSubtasks": "サブタスクを折りたたむ" + "collapseSubtasks": "サブタスクを折りたたむ", + "delegatedTag": "サブタスク待ち", + "interruptedTag": "中断" } diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index d826bf7ca8..5b054bc9ce 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -17,7 +17,9 @@ "condenseContext": "컨텍스트 지능적으로 압축", "openApiHistory": "API 기록 열기", "openUiHistory": "UI 기록 열기", - "backToParentTask": "상위 작업" + "backToParentTask": "상위 작업", + "waitingOnSubtask": "하위 작업 대기 중", + "goToSubtask": "하위 작업으로 이동" }, "unpin": "고정 해제하기", "pin": "고정하기", diff --git a/webview-ui/src/i18n/locales/ko/history.json b/webview-ui/src/i18n/locales/ko/history.json index 0363feaaff..8a13ff12cd 100644 --- a/webview-ui/src/i18n/locales/ko/history.json +++ b/webview-ui/src/i18n/locales/ko/history.json @@ -47,5 +47,7 @@ "subtaskTag": "부분작업", "deleteWithSubtasks": "이는 {{count}} 부분작업도 삭제합니다. 확실하십니까?", "expandSubtasks": "부분작업 확장", - "collapseSubtasks": "부분작업 축소" + "collapseSubtasks": "부분작업 축소", + "delegatedTag": "하위 작업 대기 중", + "interruptedTag": "중단됨" } diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index ad489050e7..e8fdff65de 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Context intelligent samenvatten", "openApiHistory": "API-geschiedenis openen", "openUiHistory": "UI-geschiedenis openen", - "backToParentTask": "Bovenliggende taak" + "backToParentTask": "Bovenliggende taak", + "waitingOnSubtask": "Wacht op subtaak", + "goToSubtask": "Ga naar subtaak" }, "unpin": "Losmaken", "pin": "Vastmaken", diff --git a/webview-ui/src/i18n/locales/nl/history.json b/webview-ui/src/i18n/locales/nl/history.json index 012059ed04..db1515bfe5 100644 --- a/webview-ui/src/i18n/locales/nl/history.json +++ b/webview-ui/src/i18n/locales/nl/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Subtaak", "deleteWithSubtasks": "Dit zal ook {{count}} subtaak(en) verwijderen. Weet je het zeker?", "expandSubtasks": "Subtaken uitvouwen", - "collapseSubtasks": "Subtaken samenvouwen" + "collapseSubtasks": "Subtaken samenvouwen", + "delegatedTag": "Wacht op subtaak", + "interruptedTag": "Onderbroken" } diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 46da82306c..a7ba2dc26a 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Inteligentnie skondensuj kontekst", "openApiHistory": "Otwórz historię API", "openUiHistory": "Otwórz historię UI", - "backToParentTask": "Zadanie nadrzędne" + "backToParentTask": "Zadanie nadrzędne", + "waitingOnSubtask": "Oczekuje na podzadanie", + "goToSubtask": "Przejdź do podzadania" }, "unpin": "Odepnij", "pin": "Przypnij", diff --git a/webview-ui/src/i18n/locales/pl/history.json b/webview-ui/src/i18n/locales/pl/history.json index 7ec4b40d8f..2924d4710e 100644 --- a/webview-ui/src/i18n/locales/pl/history.json +++ b/webview-ui/src/i18n/locales/pl/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Podzadanie", "deleteWithSubtasks": "Spowoduje to usunięcie {{count}} podzadania(ń). Jesteś pewny?", "expandSubtasks": "Rozwiń podzadania", - "collapseSubtasks": "Zwiń podzadania" + "collapseSubtasks": "Zwiń podzadania", + "delegatedTag": "Oczekuje na podzadanie", + "interruptedTag": "Przerwane" } diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 83452930d6..b70d5804f5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Condensar contexto de forma inteligente", "openApiHistory": "Abrir histórico da API", "openUiHistory": "Abrir histórico da UI", - "backToParentTask": "Tarefa pai" + "backToParentTask": "Tarefa pai", + "waitingOnSubtask": "Aguardando subtarefa", + "goToSubtask": "Ir para subtarefa" }, "unpin": "Desfixar", "pin": "Fixar", diff --git a/webview-ui/src/i18n/locales/pt-BR/history.json b/webview-ui/src/i18n/locales/pt-BR/history.json index 7966df1f46..79c84b70ef 100644 --- a/webview-ui/src/i18n/locales/pt-BR/history.json +++ b/webview-ui/src/i18n/locales/pt-BR/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Subtarefa", "deleteWithSubtasks": "Isso também excluirá {{count}} subtarefa(s). Tem certeza?", "expandSubtasks": "Expandir subtarefas", - "collapseSubtasks": "Recolher subtarefas" + "collapseSubtasks": "Recolher subtarefas", + "delegatedTag": "Aguardando subtarefa", + "interruptedTag": "Interrompida" } diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 4e09e1e704..7b8a57d722 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Интеллектуально сжать контекст", "openApiHistory": "Открыть историю API", "openUiHistory": "Открыть историю UI", - "backToParentTask": "Родительская задача" + "backToParentTask": "Родительская задача", + "waitingOnSubtask": "Ожидание подзадачи", + "goToSubtask": "Перейти к подзадаче" }, "unpin": "Открепить", "pin": "Закрепить", diff --git a/webview-ui/src/i18n/locales/ru/history.json b/webview-ui/src/i18n/locales/ru/history.json index 7852362348..3035ec59d9 100644 --- a/webview-ui/src/i18n/locales/ru/history.json +++ b/webview-ui/src/i18n/locales/ru/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Подзадача", "deleteWithSubtasks": "Это также удалит {{count}} подзадачу(и). Вы уверены?", "expandSubtasks": "Развернуть подзадачи", - "collapseSubtasks": "Свернуть подзадачи" + "collapseSubtasks": "Свернуть подзадачи", + "delegatedTag": "Ожидание подзадачи", + "interruptedTag": "Прервано" } diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 88f6c25968..8aaa9e58e1 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Bağlamı akıllıca yoğunlaştır", "openApiHistory": "API Geçmişini Aç", "openUiHistory": "UI Geçmişini Aç", - "backToParentTask": "Üst görev" + "backToParentTask": "Üst görev", + "waitingOnSubtask": "Alt görev bekleniyor", + "goToSubtask": "Alt göreve git" }, "unpin": "Sabitlemeyi iptal et", "pin": "Sabitle", diff --git a/webview-ui/src/i18n/locales/tr/history.json b/webview-ui/src/i18n/locales/tr/history.json index fb7b6c6832..2ebc1a0154 100644 --- a/webview-ui/src/i18n/locales/tr/history.json +++ b/webview-ui/src/i18n/locales/tr/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Alt görev", "deleteWithSubtasks": "Bu, {{count}} alt görev(i) de silecektir. Emin misiniz?", "expandSubtasks": "Alt görevleri genişlet", - "collapseSubtasks": "Alt görevleri daralt" + "collapseSubtasks": "Alt görevleri daralt", + "delegatedTag": "Alt görev bekleniyor", + "interruptedTag": "Kesintiye uğradı" } diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index f506962395..54d46edf6a 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -17,7 +17,9 @@ "condenseContext": "Cô đọng ngữ cảnh thông minh", "openApiHistory": "Mở lịch sử API", "openUiHistory": "Mở lịch sử UI", - "backToParentTask": "Nhiệm vụ cha" + "backToParentTask": "Nhiệm vụ cha", + "waitingOnSubtask": "Đang chờ nhiệm vụ con", + "goToSubtask": "Đến nhiệm vụ con" }, "unpin": "Bỏ ghim khỏi đầu", "pin": "Ghim lên đầu", diff --git a/webview-ui/src/i18n/locales/vi/history.json b/webview-ui/src/i18n/locales/vi/history.json index 779953e540..a6efa0671e 100644 --- a/webview-ui/src/i18n/locales/vi/history.json +++ b/webview-ui/src/i18n/locales/vi/history.json @@ -47,5 +47,7 @@ "subtaskTag": "Tác vụ con", "deleteWithSubtasks": "Điều này cũng sẽ xóa {{count}} tác vụ con. Bạn có chắc không?", "expandSubtasks": "Mở rộng tác vụ con", - "collapseSubtasks": "Thu gọn tác vụ con" + "collapseSubtasks": "Thu gọn tác vụ con", + "delegatedTag": "Đang chờ nhiệm vụ con", + "interruptedTag": "Bị gián đoạn" } diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 286e49c8f7..c3621c4009 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -17,7 +17,9 @@ "condenseContext": "智能压缩上下文", "openApiHistory": "打开 API 历史", "openUiHistory": "打开 UI 历史", - "backToParentTask": "父任务" + "backToParentTask": "父任务", + "waitingOnSubtask": "等待子任务", + "goToSubtask": "前往子任务" }, "unpin": "取消置顶", "pin": "置顶", diff --git a/webview-ui/src/i18n/locales/zh-CN/history.json b/webview-ui/src/i18n/locales/zh-CN/history.json index 20a73240ea..6b6bd03300 100644 --- a/webview-ui/src/i18n/locales/zh-CN/history.json +++ b/webview-ui/src/i18n/locales/zh-CN/history.json @@ -47,5 +47,7 @@ "subtaskTag": "子任务", "deleteWithSubtasks": "这也将删除 {{count}} 个子任务。您确定吗?", "expandSubtasks": "展开子任务", - "collapseSubtasks": "收起子任务" + "collapseSubtasks": "收起子任务", + "delegatedTag": "等待子任务", + "interruptedTag": "已中断" } diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 6b6aeba6fe..cf7439cd13 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -17,7 +17,9 @@ "delete": "刪除工作(按住 Shift 並點選可跳過確認)", "openApiHistory": "開啟 API 歷史紀錄", "openUiHistory": "開啟 UI 歷史紀錄", - "backToParentTask": "上層工作" + "backToParentTask": "上層工作", + "waitingOnSubtask": "等待子任務", + "goToSubtask": "前往子任務" }, "unpin": "取消釘選", "pin": "釘選", diff --git a/webview-ui/src/i18n/locales/zh-TW/history.json b/webview-ui/src/i18n/locales/zh-TW/history.json index 1e12190a69..5fb3230c80 100644 --- a/webview-ui/src/i18n/locales/zh-TW/history.json +++ b/webview-ui/src/i18n/locales/zh-TW/history.json @@ -47,5 +47,7 @@ "subtaskTag": "子工作", "deleteWithSubtasks": "這也將刪除 {{count}} 個子工作。您確定嗎?", "expandSubtasks": "展開子工作", - "collapseSubtasks": "收起子工作" + "collapseSubtasks": "收起子工作", + "delegatedTag": "等待子任務", + "interruptedTag": "已中斷" } From 7af1a8ad78d5bdac976f096df1d838b0abe4c345 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Thu, 23 Jul 2026 02:31:35 +0000 Subject: [PATCH 49/89] refactor(types): use canonical provider identifiers in categories (#989) * refactor(types): use canonical provider category identifiers * test(types): cover cross-category provider guards --- .../__tests__/provider-identifiers.test.ts | 79 +++++++++++++++++++ packages/types/src/provider-settings.ts | 30 +++---- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts index 477f26dc17..11f9d90cc7 100644 --- a/packages/types/src/__tests__/provider-identifiers.test.ts +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -1,6 +1,16 @@ import { + customProviders, + dynamicProviders, + fauxProviders, + internalProviders, + isCustomProvider, + isDynamicProvider, + isFauxProvider, + isInternalProvider, + isLocalProvider, isProviderName, isRetiredProvider, + localProviders, providerIdentifiers, providerNames, providerNamesSchema, @@ -81,6 +91,75 @@ describe("provider identifiers", () => { expect(retiredProviderNames).toEqual(retiredIdentifiers) }) + it("derives provider category collections from canonical identifiers", () => { + expect(dynamicProviders).toEqual([ + providerIdentifiers.openrouter, + providerIdentifiers.vercelAiGateway, + providerIdentifiers.zooGateway, + providerIdentifiers.litellm, + providerIdentifiers.requesty, + providerIdentifiers.unbound, + providerIdentifiers.poe, + providerIdentifiers.deepseek, + providerIdentifiers.moonshot, + providerIdentifiers.opencodeGo, + providerIdentifiers.kenari, + ]) + expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio]) + expect(internalProviders).toEqual([providerIdentifiers.vscodeLm]) + expect(customProviders).toEqual([providerIdentifiers.openai]) + expect(fauxProviders).toEqual([providerIdentifiers.fakeAi]) + }) + + it("preserves provider category type guards", () => { + for (const identifier of dynamicProviders) { + expect(isDynamicProvider(identifier)).toBe(true) + } + + for (const identifier of localProviders) { + expect(isLocalProvider(identifier)).toBe(true) + } + + for (const identifier of internalProviders) { + expect(isInternalProvider(identifier)).toBe(true) + } + + for (const identifier of customProviders) { + expect(isCustomProvider(identifier)).toBe(true) + } + + for (const identifier of fauxProviders) { + expect(isFauxProvider(identifier)).toBe(true) + } + + expect(isDynamicProvider("unknown-provider")).toBe(false) + expect(isLocalProvider("unknown-provider")).toBe(false) + expect(isInternalProvider("unknown-provider")).toBe(false) + expect(isCustomProvider("unknown-provider")).toBe(false) + expect(isFauxProvider("unknown-provider")).toBe(false) + + const categoryRepresentatives = [ + providerIdentifiers.openrouter, + providerIdentifiers.ollama, + providerIdentifiers.vscodeLm, + providerIdentifiers.openai, + providerIdentifiers.fakeAi, + ] + const categoryGuards = [ + isDynamicProvider, + isLocalProvider, + isInternalProvider, + isCustomProvider, + isFauxProvider, + ] + + for (const [guardIndex, guard] of categoryGuards.entries()) { + for (const [identifierIndex, identifier] of categoryRepresentatives.entries()) { + expect(guard(identifier)).toBe(guardIndex === identifierIndex) + } + } + }) + it("keeps active and retired providers separate", () => { const activeIdentifiers = new Set(Object.values(providerIdentifiers)) const retiredIdentifiers = Object.values(retiredProviderIdentifiers) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c9b7c01045..194273f944 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -44,17 +44,17 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 */ export const dynamicProviders = [ - "openrouter", - "vercel-ai-gateway", - "zoo-gateway", - "litellm", - "requesty", - "unbound", - "poe", - "deepseek", - "moonshot", - "opencode-go", - "kenari", + providerIdentifiers.openrouter, + providerIdentifiers.vercelAiGateway, + providerIdentifiers.zooGateway, + providerIdentifiers.litellm, + providerIdentifiers.requesty, + providerIdentifiers.unbound, + providerIdentifiers.poe, + providerIdentifiers.deepseek, + providerIdentifiers.moonshot, + providerIdentifiers.opencodeGo, + providerIdentifiers.kenari, ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -68,7 +68,7 @@ export const isDynamicProvider = (key: string): key is DynamicProvider => * Local providers require localhost API calls in order to get the model list. */ -export const localProviders = ["ollama", "lmstudio"] as const +export const localProviders = [providerIdentifiers.ollama, providerIdentifiers.lmstudio] as const export type LocalProvider = (typeof localProviders)[number] @@ -81,7 +81,7 @@ export const isLocalProvider = (key: string): key is LocalProvider => localProvi * model list. */ -export const internalProviders = ["vscode-lm"] as const +export const internalProviders = [providerIdentifiers.vscodeLm] as const export type InternalProvider = (typeof internalProviders)[number] @@ -94,7 +94,7 @@ export const isInternalProvider = (key: string): key is InternalProvider => * Custom providers are completely configurable within Roo Code settings. */ -export const customProviders = ["openai"] as const +export const customProviders = [providerIdentifiers.openai] as const export type CustomProvider = (typeof customProviders)[number] @@ -107,7 +107,7 @@ export const isCustomProvider = (key: string): key is CustomProvider => customPr * model lists. */ -export const fauxProviders = ["fake-ai"] as const +export const fauxProviders = [providerIdentifiers.fakeAi] as const export type FauxProvider = (typeof fauxProviders)[number] From 6fec173bfc68bc2c62d8d0b2f1e9b9b56fb8e45d Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:58:01 -0700 Subject: [PATCH 50/89] [Feat] Add Kimi K3 model to Moonshot and OpenCode Go providers (#996) * feat(types): add Kimi K3 to moonshot and opencode-go providers * test(types): add moonshot registry suite and explicit kimi-k3 reasoning assertions * test(types): widen moonshot registry entries to ModelInfo in tests --------- Co-authored-by: Roomote --- packages/types/src/__tests__/lite-llm.test.ts | 1 + packages/types/src/__tests__/moonshot.test.ts | 72 +++++++++++++++++++ .../types/src/__tests__/opencode-go.test.ts | 18 +++++ packages/types/src/providers/lite-llm.ts | 3 + packages/types/src/providers/moonshot.ts | 15 ++++ packages/types/src/providers/opencode-go.ts | 17 +++++ 6 files changed, 126 insertions(+) create mode 100644 packages/types/src/__tests__/moonshot.test.ts diff --git a/packages/types/src/__tests__/lite-llm.test.ts b/packages/types/src/__tests__/lite-llm.test.ts index 03424b2e57..74436c1848 100644 --- a/packages/types/src/__tests__/lite-llm.test.ts +++ b/packages/types/src/__tests__/lite-llm.test.ts @@ -12,6 +12,7 @@ describe("LiteLLM preserveReasoning model detection", () => { }) it("matches provider-prefixed routed model names by their final segment", () => { + expect(isLiteLLMPreserveReasoningModel("kimi-k3")).toBe(true) expect(isLiteLLMPreserveReasoningModel("deepseek/deepseek-reasoner")).toBe(true) expect(isLiteLLMPreserveReasoningModel("bedrock/moonshot.kimi-k2-thinking")).toBe(true) expect(isLiteLLMPreserveReasoningModel("fireworks_ai/accounts/fireworks/models/kimi-k2p7-code")).toBe(true) diff --git a/packages/types/src/__tests__/moonshot.test.ts b/packages/types/src/__tests__/moonshot.test.ts new file mode 100644 index 0000000000..61cafd3167 --- /dev/null +++ b/packages/types/src/__tests__/moonshot.test.ts @@ -0,0 +1,72 @@ +import type { ModelInfo } from "../model.js" +import { MOONSHOT_DEFAULT_TEMPERATURE, moonshotDefaultModelId, moonshotModels } from "../providers/moonshot.js" + +const modelEntries: [string, ModelInfo][] = Object.entries(moonshotModels) +const modelInfos: ModelInfo[] = Object.values(moonshotModels) + +describe("moonshot registry", () => { + describe("moonshotModels registry invariants", () => { + it("every entry has a positive maxTokens and contextWindow", () => { + for (const [id, info] of modelEntries) { + expect(info.maxTokens).toBeGreaterThan(0) + expect(info.contextWindow).toBeGreaterThan(0) + // Sanity: max output must not exceed the context window. + expect(info.maxTokens).toBeLessThanOrEqual(info.contextWindow) + void id + } + }) + + it("every entry declares supportsImages and supportsPromptCache", () => { + for (const info of modelInfos) { + expect(typeof info.supportsImages).toBe("boolean") + expect(typeof info.supportsPromptCache).toBe("boolean") + } + }) + + it("models with an array supportsReasoningEffort expose a non-empty allow-list", () => { + for (const info of modelInfos) { + if (Array.isArray(info.supportsReasoningEffort)) { + expect(info.supportsReasoningEffort.length).toBeGreaterThan(0) + } + } + }) + + it("every entry declares a reasoningEffort that is covered by its allow-list", () => { + for (const info of modelInfos) { + if (Array.isArray(info.supportsReasoningEffort) && info.reasoningEffort !== undefined) { + expect(info.supportsReasoningEffort).toContain(info.reasoningEffort) + } + } + }) + }) + + describe("kimi-k3", () => { + it("exposes always-on reasoning with effort allow-list and reasoning preservation", () => { + const info = moonshotModels["kimi-k3"] + expect(info).toBeDefined() + expect(info.maxTokens).toBe(131_072) + expect(info.contextWindow).toBe(1_048_576) + expect(info.supportsImages).toBe(true) + expect(info.supportsPromptCache).toBe(true) + expect(info.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(info.reasoningEffort).toBe("max") + expect(info.preserveReasoning).toBe(true) + expect(info.defaultTemperature).toBe(1.0) + expect(info.inputPrice).toBe(3.0) + expect(info.outputPrice).toBe(15.0) + expect(info.cacheWritesPrice).toBe(0) + expect(info.cacheReadsPrice).toBe(0.3) + }) + }) + + describe("defaults", () => { + it("the default model id is a curated registry entry", () => { + expect(moonshotDefaultModelId).toBe("kimi-k2-0905-preview") + expect(moonshotModels[moonshotDefaultModelId]).toBeDefined() + }) + + it("exposes a deterministic default temperature", () => { + expect(MOONSHOT_DEFAULT_TEMPERATURE).toBe(0.6) + }) + }) +}) diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 617c88675c..8079fe1bcb 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -21,6 +21,7 @@ describe("opencode-go registry", () => { "glm-5", "glm-5.1", "glm-5.2", + "kimi-k3", "kimi-k2.5", "kimi-k2.6", "mimo-v2.5", @@ -60,6 +61,23 @@ describe("opencode-go registry", () => { it("returns undefined for an unknown model ID", () => { expect(getOpencodeGoModelInfo("not-a-real-go-model")).toBeUndefined() }) + + it("kimi-k3 exposes always-on reasoning with effort allow-list and reasoning preservation", () => { + const info = getOpencodeGoModelInfo("kimi-k3") + expect(info).toBeDefined() + expect(info?.maxTokens).toBe(131_072) + expect(info?.contextWindow).toBe(1_048_576) + expect(info?.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(info?.reasoningEffort).toBe("max") + expect(info?.preserveReasoning).toBe(true) + expect(info?.defaultTemperature).toBe(1.0) + expect(info?.supportsPromptCache).toBe(true) + expect(info?.supportsMaxTokens).toBe(true) + expect(info?.supportsImages).toBe(false) + expect(info?.inputPrice).toBe(3.0) + expect(info?.outputPrice).toBe(15.0) + expect(info?.cacheReadsPrice).toBe(0.3) + }) }) describe("OPENCODE_GO_ANTHROPIC_FORMAT_MODELS", () => { diff --git a/packages/types/src/providers/lite-llm.ts b/packages/types/src/providers/lite-llm.ts index a63f3971f5..2b72de9d51 100644 --- a/packages/types/src/providers/lite-llm.ts +++ b/packages/types/src/providers/lite-llm.ts @@ -48,6 +48,9 @@ export const LITELLM_PRESERVE_REASONING_MODEL_IDS = [ "moonshot.kimi-k2-thinking", "kimi-k2p7-code", + // moonshot.ts, opencode-go.ts + "kimi-k3", + // zai.ts "glm-4.7", "glm-5", diff --git a/packages/types/src/providers/moonshot.ts b/packages/types/src/providers/moonshot.ts index ad729bc2a7..8e041d160a 100644 --- a/packages/types/src/providers/moonshot.ts +++ b/packages/types/src/providers/moonshot.ts @@ -6,6 +6,21 @@ export type MoonshotModelId = keyof typeof moonshotModels export const moonshotDefaultModelId: MoonshotModelId = "kimi-k2-0905-preview" export const moonshotModels = { + "kimi-k3": { + maxTokens: 131_072, // Default max_completion_tokens (configurable up to 1,048,576) + contextWindow: 1_048_576, // 1M tokens + supportsImages: true, // Native visual understanding (text, image, video) + supportsPromptCache: true, // Automatic context caching + supportsReasoningEffort: ["low", "high", "max"], // Always reasons; default "max" + reasoningEffort: "max", + preserveReasoning: true, + inputPrice: 3.0, // $3.00 per million tokens (cache miss) + outputPrice: 15.0, // $15.00 per million tokens + cacheWritesPrice: 0, // $0 per million tokens (cache miss) + cacheReadsPrice: 0.3, // $0.30 per million tokens (cache hit) + defaultTemperature: 1.0, // temperature is fixed at 1.0 + description: `Kimi K3 is Kimi's most capable flagship model with 2.8 trillion parameters, native visual understanding, and a 1M-token context window, designed for long-horizon coding, knowledge work, and deep reasoning. Thinking is always enabled with configurable reasoning effort (low/high/max, default max).`, + }, "kimi-k2-0711-preview": { maxTokens: 32_000, contextWindow: 131_072, diff --git a/packages/types/src/providers/opencode-go.ts b/packages/types/src/providers/opencode-go.ts index 5b1e0ada8e..bd60c4d349 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -104,6 +104,23 @@ export const opencodeGoModels: Record = { }, // --- Moonshot Kimi --- + "kimi-k3": { + maxTokens: 131_072, // Default max_completion_tokens (configurable up to 1,048,576) + contextWindow: 1_048_576, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], // Always reasons; default "max" + reasoningEffort: "max", + preserveReasoning: true, + defaultTemperature: 1.0, + // Go pricing matches Moonshot direct ($3 in / $0.30 cache / $15 out per 1M tokens). + inputPrice: 3.0, + outputPrice: 15.0, + cacheReadsPrice: 0.3, + description: + "Kimi K3 is Moonshot AI's flagship model with 2.8 trillion parameters, a 1M context window, and always-on reasoning with configurable effort (low/high/max). Available via the Opencode Go plan.", + }, "kimi-k2.5": { maxTokens: 16_384, contextWindow: 262_144, From 78c1410d1db98af07639b8d5238932f21e0a8a4b Mon Sep 17 00:00:00 2001 From: T Date: Fri, 24 Jul 2026 10:35:53 -0400 Subject: [PATCH 51/89] [Feat] Add Kimi Code provider with OAuth device flow (#945) * feat: add Kimi Code provider * no-mistakes(review): add explicit error handling for OAuth retry path * no-mistakes(document): Fix incorrect provider attribution in OAuth comment * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix(kimi-code): harden OAuth request handling * feat(kimi-code): add configurable reasoning effort (low/high/max) * fix(kimi-code): address review feedback and resolve main merge conflicts - Wrap kimiCodeOAuthState aggregation in try/catch in ClineProvider - Add non-401 OAuth no-retry test for createMessage - Add vi.resetModules() to kimiCode sign-in/out spec setup - Localize kimiCode settings strings in 9 locales - Merge origin/main: adopt canonical providerIdentifiers registry, keep both kimi-code and zoo-gateway setup-gate checks --------- Co-authored-by: Toray Altas <6816042+taltas@users.noreply.github.com> Co-authored-by: Roomote Co-authored-by: Naved Merchant --- .../types/src/__tests__/kimi-code.test.ts | 29 ++ .../__tests__/provider-identifiers.test.ts | 2 + packages/types/src/global-settings.ts | 1 + packages/types/src/model.ts | 1 + packages/types/src/provider-identifiers.ts | 1 + packages/types/src/provider-settings.ts | 17 + packages/types/src/providers/index.ts | 4 + packages/types/src/providers/kimi-code.ts | 23 + packages/types/src/vscode-extension-host.ts | 10 + src/api/index.ts | 3 + src/api/providers/__tests__/kimi-code.spec.ts | 236 +++++++++ src/api/providers/__tests__/openai.spec.ts | 6 + .../fetchers/__tests__/kimi-code.spec.ts | 100 ++++ src/api/providers/fetchers/kimi-code.ts | 58 +++ src/api/providers/fetchers/modelCache.ts | 6 +- src/api/providers/index.ts | 1 + src/api/providers/kimi-code.ts | 118 +++++ src/api/providers/openai.ts | 8 +- src/api/transform/__tests__/reasoning.spec.ts | 51 ++ src/api/transform/reasoning.ts | 19 + src/core/webview/ClineProvider.ts | 16 + .../webview/__tests__/ClineProvider.spec.ts | 3 + .../__tests__/webviewMessageHandler.spec.ts | 148 ++++++ src/core/webview/webviewMessageHandler.ts | 56 +++ src/extension.ts | 3 + .../kimi-code/__tests__/oauth.spec.ts | 465 ++++++++++++++++++ src/integrations/kimi-code/oauth.ts | 379 ++++++++++++++ .../__tests__/checkExistApiConfig.spec.ts | 32 ++ src/shared/api.ts | 1 + src/shared/checkExistApiConfig.ts | 4 + .../src/components/settings/ApiOptions.tsx | 13 +- .../src/components/settings/constants.ts | 1 + .../settings/providers/KimiCode.tsx | 152 ++++++ .../providers/__tests__/KimiCode.spec.tsx | 41 ++ .../components/settings/providers/index.ts | 1 + .../settings/utils/providerModelConfig.ts | 5 + .../components/ui/hooks/useSelectedModel.ts | 14 +- webview-ui/src/i18n/locales/ca/settings.json | 11 + webview-ui/src/i18n/locales/de/settings.json | 11 + webview-ui/src/i18n/locales/en/settings.json | 11 + webview-ui/src/i18n/locales/es/settings.json | 11 + webview-ui/src/i18n/locales/fr/settings.json | 11 + webview-ui/src/i18n/locales/hi/settings.json | 11 + webview-ui/src/i18n/locales/id/settings.json | 11 + webview-ui/src/i18n/locales/it/settings.json | 11 + webview-ui/src/i18n/locales/ja/settings.json | 11 + webview-ui/src/i18n/locales/ko/settings.json | 11 + webview-ui/src/i18n/locales/nl/settings.json | 11 + webview-ui/src/i18n/locales/pl/settings.json | 11 + .../src/i18n/locales/pt-BR/settings.json | 11 + webview-ui/src/i18n/locales/ru/settings.json | 11 + webview-ui/src/i18n/locales/tr/settings.json | 11 + webview-ui/src/i18n/locales/vi/settings.json | 11 + .../src/i18n/locales/zh-CN/settings.json | 11 + .../src/i18n/locales/zh-TW/settings.json | 11 + .../src/utils/__tests__/validate.spec.ts | 43 ++ webview-ui/src/utils/validate.ts | 5 + 57 files changed, 2270 insertions(+), 4 deletions(-) create mode 100644 packages/types/src/__tests__/kimi-code.test.ts create mode 100644 packages/types/src/providers/kimi-code.ts create mode 100644 src/api/providers/__tests__/kimi-code.spec.ts create mode 100644 src/api/providers/fetchers/__tests__/kimi-code.spec.ts create mode 100644 src/api/providers/fetchers/kimi-code.ts create mode 100644 src/api/providers/kimi-code.ts create mode 100644 src/integrations/kimi-code/__tests__/oauth.spec.ts create mode 100644 src/integrations/kimi-code/oauth.ts create mode 100644 webview-ui/src/components/settings/providers/KimiCode.tsx create mode 100644 webview-ui/src/components/settings/providers/__tests__/KimiCode.spec.tsx diff --git a/packages/types/src/__tests__/kimi-code.test.ts b/packages/types/src/__tests__/kimi-code.test.ts new file mode 100644 index 0000000000..3d8b9ac549 --- /dev/null +++ b/packages/types/src/__tests__/kimi-code.test.ts @@ -0,0 +1,29 @@ +import { + SECRET_STATE_KEYS, + dynamicProviders, + kimiCodeDefaultModelId, + providerSettingsSchema, + providerSettingsSchemaDiscriminated, +} from "../index.js" + +describe("Kimi Code provider types", () => { + it("registers Kimi Code as a dynamic provider with a distinct secret", () => { + expect(dynamicProviders).toContain("kimi-code") + expect(SECRET_STATE_KEYS).toContain("kimiCodeApiKey") + expect(SECRET_STATE_KEYS).toContain("moonshotApiKey") + }) + + it("parses OAuth and API-key settings independently from Moonshot", () => { + expect( + providerSettingsSchemaDiscriminated.parse({ + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "kimi-key", + apiModelId: kimiCodeDefaultModelId, + }), + ).toMatchObject({ kimiCodeApiKey: "kimi-key" }) + expect(providerSettingsSchema.parse({ apiProvider: "kimi-code", kimiCodeAuthMethod: "oauth" })).toMatchObject({ + kimiCodeAuthMethod: "oauth", + }) + }) +}) diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts index 11f9d90cc7..b3640a8f5d 100644 --- a/packages/types/src/__tests__/provider-identifiers.test.ts +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -45,6 +45,7 @@ const expectedProviderIdentifiers = [ "gemini-cli", "mistral", "moonshot", + "kimi-code", "minimax", "mimo", "openai-codex", @@ -104,6 +105,7 @@ describe("provider identifiers", () => { providerIdentifiers.moonshot, providerIdentifiers.opencodeGo, providerIdentifiers.kenari, + providerIdentifiers.kimiCode, ]) expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio]) expect(internalProviders).toEqual([providerIdentifiers.vscodeLm]) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 76420bc020..88b5408fca 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -300,6 +300,7 @@ export const SECRET_STATE_KEYS = [ "openAiNativeApiKey", "deepSeekApiKey", "moonshotApiKey", + "kimiCodeApiKey", "mistralApiKey", "minimaxApiKey", "requestyApiKey", diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 87ebfaf967..257a30f3e7 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -122,6 +122,7 @@ export const modelInfoSchema = z.object({ }) .optional(), description: z.string().optional(), + displayName: z.string().optional(), // Default effort value for models that support reasoning effort reasoningEffort: reasoningEffortExtendedSchema.optional(), minTokensPerCachePoint: z.number().optional(), diff --git a/packages/types/src/provider-identifiers.ts b/packages/types/src/provider-identifiers.ts index 30bfb9d17f..f231bc1ab1 100644 --- a/packages/types/src/provider-identifiers.ts +++ b/packages/types/src/provider-identifiers.ts @@ -28,6 +28,7 @@ export const providerIdentifiers = { geminiCli: "gemini-cli", mistral: "mistral", moonshot: "moonshot", + kimiCode: "kimi-code", minimax: "minimax", mimo: "mimo", openaiCodex: "openai-codex", diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 194273f944..3898c65bcc 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -55,6 +55,7 @@ export const dynamicProviders = [ providerIdentifiers.moonshot, providerIdentifiers.opencodeGo, providerIdentifiers.kenari, + providerIdentifiers.kimiCode, ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -309,6 +310,14 @@ const moonshotSchema = apiModelIdProviderModelSchema.extend({ moonshotApiKey: z.string().optional(), }) +export const kimiCodeAuthMethodSchema = z.enum(["oauth", "api-key"]) +export type KimiCodeAuthMethod = z.infer + +const kimiCodeSchema = apiModelIdProviderModelSchema.extend({ + kimiCodeAuthMethod: kimiCodeAuthMethodSchema.optional(), + kimiCodeApiKey: z.string().optional(), +}) + const minimaxSchema = apiModelIdProviderModelSchema.extend({ minimaxBaseUrl: z .union([z.literal("https://api.minimax.io/v1"), z.literal("https://api.minimaxi.com/v1")]) @@ -425,6 +434,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), poeSchema.merge(z.object({ apiProvider: z.literal("poe") })), moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), + kimiCodeSchema.merge(z.object({ apiProvider: z.literal("kimi-code") })), minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })), mimoSchema.merge(z.object({ apiProvider: z.literal("mimo") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), @@ -463,6 +473,7 @@ export const providerSettingsSchema = z.object({ ...deepSeekSchema.shape, ...poeSchema.shape, ...moonshotSchema.shape, + ...kimiCodeSchema.shape, ...minimaxSchema.shape, ...mimoSchema.shape, ...requestySchema.shape, @@ -544,6 +555,7 @@ export const modelIdKeysByProvider: Record = { "gemini-cli": "apiModelId", mistral: "apiModelId", moonshot: "apiModelId", + "kimi-code": "apiModelId", minimax: "apiModelId", mimo: "apiModelId", deepseek: "apiModelId", @@ -652,6 +664,11 @@ export const MODELS_BY_PROVIDER: Record< label: "Moonshot", models: Object.keys(moonshotModels), }, + "kimi-code": { + id: "kimi-code", + label: "Kimi Code", + models: [], + }, minimax: { id: "minimax", label: "MiniMax", diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 77b0415898..9a78de11ac 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -25,6 +25,7 @@ export * from "./xai.js" export * from "./vercel-ai-gateway.js" export * from "./opencode-go.js" export * from "./kenari.js" +export * from "./kimi-code.js" export * from "./zai.js" export * from "./minimax.js" export * from "./mimo.js" @@ -53,6 +54,7 @@ import { xaiDefaultModelId } from "./xai.js" import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { opencodeGoDefaultModelId } from "./opencode-go.js" import { kenariDefaultModelId } from "./kenari.js" +import { kimiCodeDefaultModelId } from "./kimi-code.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" import { minimaxDefaultModelId } from "./minimax.js" import { mimoDefaultModelId } from "./mimo.js" @@ -129,6 +131,8 @@ export function getProviderDefaultModelId( return opencodeGoDefaultModelId case "kenari": return kenariDefaultModelId + case "kimi-code": + return kimiCodeDefaultModelId case "zoo-gateway": return zooGatewayDefaultModelId case "anthropic": diff --git a/packages/types/src/providers/kimi-code.ts b/packages/types/src/providers/kimi-code.ts new file mode 100644 index 0000000000..c9a458ed1f --- /dev/null +++ b/packages/types/src/providers/kimi-code.ts @@ -0,0 +1,23 @@ +import type { ModelInfo } from "../model.js" + +export const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1" +export const kimiCodeDefaultModelId = "kimi-for-coding" + +export const kimiCodeReasoningEfforts = ["low", "high", "max"] as const + +export const kimiCodeDefaultModelInfo: ModelInfo = { + contextWindow: 262_144, + maxTokens: 32_768, + supportsImages: false, + supportsPromptCache: false, + supportsReasoningEffort: [...kimiCodeReasoningEfforts], + requiredReasoningEffort: true, + reasoningEffort: "max", + description: "Kimi Code's coding model for subscription and API-key access.", +} + +export const kimiCodeModels = { + [kimiCodeDefaultModelId]: kimiCodeDefaultModelInfo, +} as const satisfies Record + +export type KimiCodeModelId = keyof typeof kimiCodeModels diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 9bd53f907a..c35a5da538 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -389,6 +389,14 @@ export type ExtensionState = Pick< mdmCompliant?: boolean taskSyncEnabled: boolean openAiCodexIsAuthenticated?: boolean + kimiCodeIsAuthenticated?: boolean + kimiCodeOAuthState?: { + status: "idle" | "authorizing" | "polling" | "authenticated" | "error" + userCode?: string + verificationUri?: string + expiresAt?: number + error?: string + } zooCodeIsAuthenticated?: boolean zooCodeUserName?: string zooCodeUserEmail?: string @@ -538,6 +546,8 @@ export interface WebviewMessage { | "rooCloudManualUrl" | "openAiCodexSignIn" | "openAiCodexSignOut" + | "kimiCodeSignIn" + | "kimiCodeSignOut" | "zooCodeSignOut" | "switchOrganization" | "condenseTaskContextRequest" diff --git a/src/api/index.ts b/src/api/index.ts index 48a070feb4..fccbf48867 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,6 +20,7 @@ import { OpenAiNativeHandler, DeepSeekHandler, MoonshotHandler, + KimiCodeHandler, MistralHandler, VsCodeLmHandler, RequestyHandler, @@ -178,6 +179,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new QwenCodeHandler(options) case "moonshot": return new MoonshotHandler(options) + case "kimi-code": + return new KimiCodeHandler(options) case "vscode-lm": return new VsCodeLmHandler(options) case "mistral": diff --git a/src/api/providers/__tests__/kimi-code.spec.ts b/src/api/providers/__tests__/kimi-code.spec.ts new file mode 100644 index 0000000000..d78c252304 --- /dev/null +++ b/src/api/providers/__tests__/kimi-code.spec.ts @@ -0,0 +1,236 @@ +import { buildApiHandler } from "../../index" +import { KimiCodeHandler } from "../kimi-code" + +const { mockGetAccessToken, mockForceRefreshAccessToken, mockGetModels } = vi.hoisted(() => ({ + mockGetAccessToken: vi.fn(), + mockForceRefreshAccessToken: vi.fn(), + mockGetModels: vi.fn(), +})) + +vi.mock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + getAccessToken: mockGetAccessToken, + forceRefreshAccessToken: mockForceRefreshAccessToken, + }, +})) + +vi.mock("../fetchers/modelCache", () => ({ getModels: mockGetModels })) + +describe("KimiCodeHandler", () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetAccessToken.mockResolvedValue("oauth-token") + mockForceRefreshAccessToken.mockResolvedValue("refreshed-token") + mockGetModels.mockRejectedValue(new Error("offline")) + }) + + it("is dispatched separately from Moonshot and preserves an unknown selected model", () => { + const handler = buildApiHandler({ + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "kimi-key", + apiModelId: "future-kimi-model", + }) + expect(handler).toBeInstanceOf(KimiCodeHandler) + expect(handler.getModel().id).toBe("future-kimi-model") + }) + + it("uses kimi-for-coding only when no model is selected", () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "kimi-key" }) + expect(handler.getModel().id).toBe("kimi-for-coding") + }) + + it("uses API key when auth method is api-key", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "my-api-key" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ choices: [{ message: { content: "response" }, finish_reason: "stop" }] }), { + status: 200, + }), + ) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected - mock is incomplete + } + expect(mockGetAccessToken).not.toHaveBeenCalled() + }) + + it("uses OAuth token when auth method is oauth or not specified", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected - mock will fail + } + expect(mockGetAccessToken).toHaveBeenCalled() + }) + + it("throws error when OAuth is required but no token available", async () => { + mockGetAccessToken.mockResolvedValueOnce(null) + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + await expect(async () => { + for await (const chunk of gen) { + // consume + } + }).rejects.toThrow("Not authenticated with Kimi Code") + }) + + it("throws error when API key auth is missing the key", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + await expect(async () => { + for await (const chunk of gen) { + // consume + } + }).rejects.toThrow("Kimi Code API key is required") + }) + + it("retries with forced refresh on 401 when using OAuth", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" }) + const fetchSpy = vi.spyOn(globalThis, "fetch") + fetchSpy.mockResolvedValueOnce(new Response(null, { status: 401 })) + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify({ choices: [{ message: { content: "ok" }, finish_reason: "stop" }] }), { + status: 200, + }), + ) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected - mock is incomplete + } + expect(mockForceRefreshAccessToken).toHaveBeenCalledOnce() + }) + + it("force-refreshes and retries exactly once after a non-streaming OAuth 401", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" }) + const unauthorized = Object.assign(new Error("Unauthorized"), { status: 401 }) + const createCompletion = vi + .spyOn((handler as any).client.chat.completions, "create") + .mockRejectedValueOnce(unauthorized) + .mockResolvedValueOnce({ choices: [{ message: { content: "retried" } }] }) + + await expect(handler.completePrompt("test")).resolves.toBe("retried") + expect(mockForceRefreshAccessToken).toHaveBeenCalledOnce() + expect(createCompletion).toHaveBeenCalledTimes(2) + }) + + it("does not retry on 401 when using API key auth", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(null, { status: 401 })) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + await expect(async () => { + for await (const chunk of gen) { + // consume + } + }).rejects.toThrow() + expect(mockForceRefreshAccessToken).not.toHaveBeenCalled() + }) + + it("does not force-refresh on non-401 OAuth failures", async () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" }) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(null, { status: 500 })) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + await expect(async () => { + for await (const chunk of gen) { + // consume + } + }).rejects.toThrow() + expect(mockForceRefreshAccessToken).not.toHaveBeenCalled() + }) + + it("fetches models during prepareRequest", async () => { + mockGetModels.mockResolvedValueOnce({ "test-model": { maxTokens: 1000 } }) + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected + } + expect(mockGetModels).toHaveBeenCalled() + }) + + it("continues when model discovery fails", async () => { + mockGetModels.mockRejectedValueOnce(new Error("discovery failed")) + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected - different error + } + expect(mockGetModels).toHaveBeenCalled() + }) + + it.each([ + ["failure", () => Promise.reject(new Error("offline"))], + ["empty response", () => Promise.resolve({})], + ])("does not repeatedly block requests after model discovery %s", async (_case, discovery) => { + mockGetModels.mockImplementationOnce(discovery) + vi.spyOn(globalThis, "fetch").mockImplementation( + async () => new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), { status: 200 }), + ) + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + + await handler.completePrompt("first") + await handler.completePrompt("second") + + expect(mockGetModels).toHaveBeenCalledOnce() + }) + + it("uses discovered model info when available", async () => { + mockGetModels.mockResolvedValueOnce({ "kimi-for-coding": { maxTokens: 8000, contextWindow: 128000 } }) + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + const gen = handler.createMessage("system", [{ role: "user", content: "test" }]) + try { + for await (const chunk of gen) { + // consume + } + } catch { + // expected + } + const model = handler.getModel() + expect(model.info.maxTokens).toBe(8000) + }) + + it("defaults to max reasoning effort and advertises low/high/max support", () => { + const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" }) + const model = handler.getModel() + expect(model.info.supportsReasoningEffort).toEqual(["low", "high", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.reasoning).toEqual({ reasoning_effort: "max" }) + }) + + it("sends the user-selected reasoning effort", () => { + const handler = new KimiCodeHandler({ + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "key", + reasoningEffort: "low", + }) + expect(handler.getModel().reasoning).toEqual({ reasoning_effort: "low" }) + }) + + it("falls back to the model default when a persisted effort from another provider is unsupported", () => { + const handler = new KimiCodeHandler({ + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "key", + reasoningEffort: "medium", + }) + expect(handler.getModel().reasoning).toEqual({ reasoning_effort: "max" }) + }) +}) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 332fc39602..9c79e56159 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -905,6 +905,12 @@ describe("OpenAiHandler", () => { await expect(handler.completePrompt("Test prompt")).rejects.toThrow("OpenAI completion error: API Error") }) + it("should preserve HTTP status when wrapping completion errors", async () => { + mockCreate.mockRejectedValueOnce(Object.assign(new Error("Unauthorized"), { status: 401 })) + + await expect(handler.completePrompt("Test prompt")).rejects.toMatchObject({ status: 401 }) + }) + it("should handle empty response", async () => { mockCreate.mockImplementationOnce(() => ({ choices: [{ message: { content: "" } }], diff --git a/src/api/providers/fetchers/__tests__/kimi-code.spec.ts b/src/api/providers/fetchers/__tests__/kimi-code.spec.ts new file mode 100644 index 0000000000..3fbe995752 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/kimi-code.spec.ts @@ -0,0 +1,100 @@ +import { getKimiCodeModels, mapKimiCodeModel } from "../kimi-code" + +describe("Kimi Code model discovery", () => { + beforeEach(() => vi.restoreAllMocks()) + afterEach(() => vi.useRealTimers()) + + it("maps official model fields", () => { + expect( + mapKimiCodeModel({ + id: "kimi-test", + context_length: 131072, + supports_reasoning: true, + supports_image_in: true, + display_name: "Kimi Test", + }), + ).toMatchObject({ + contextWindow: 131072, + supportsReasoningEffort: ["low", "high", "max"], + requiredReasoningEffort: true, + reasoningEffort: "max", + supportsImages: true, + displayName: "Kimi Test", + }) + }) + + it("uses bearer auth for GET /models", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ data: [{ id: "kimi-for-coding", context_length: 262144 }] }), { + status: 200, + }), + ) + const models = await getKimiCodeModels("secret-token") + expect(models).toHaveProperty("kimi-for-coding") + expect(fetch).toHaveBeenCalledWith( + "https://api.kimi.com/coding/v1/models", + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer secret-token" }) }), + ) + }) + + it("applies default values when optional fields are missing", () => { + const mapped = mapKimiCodeModel({ + id: "basic-model", + }) + expect(mapped.supportsReasoningEffort).toBe(false) + expect(mapped.requiredReasoningEffort).toBe(false) + expect(mapped.reasoningEffort).toBeUndefined() + expect(mapped.supportsImages).toBe(false) + expect(mapped.contextWindow).toBeGreaterThan(0) + }) + + it("throws error when apiKey is missing", async () => { + await expect(getKimiCodeModels()).rejects.toThrow("Kimi Code authentication is required") + }) + + it("throws error with status code on failed request", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }), + ) + try { + await getKimiCodeModels("bad-token") + expect.fail("should have thrown") + } catch (error: any) { + expect(error.message).toContain("401") + expect(error.status).toBe(401) + } + }) + + it("returns multiple models as a record", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + data: [ + { id: "model-a", context_length: 100000 }, + { id: "model-b", context_length: 200000, supports_reasoning: true }, + ], + }), + { status: 200 }, + ), + ) + const models = await getKimiCodeModels("token") + expect(Object.keys(models)).toHaveLength(2) + expect(models["model-a"].contextWindow).toBe(100000) + expect(models["model-b"].supportsReasoningEffort).toEqual(["low", "high", "max"]) + }) + + it("aborts model discovery after its deadline", async () => { + vi.useFakeTimers() + vi.spyOn(globalThis, "fetch").mockImplementation((_input, init) => { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }) + }) + }) + const result = expect(getKimiCodeModels("token")).rejects.toThrow("timed out") + + await vi.advanceTimersByTimeAsync(10_000) + await result + expect(vi.mocked(fetch).mock.calls[0][1]?.signal?.aborted).toBe(true) + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/src/api/providers/fetchers/kimi-code.ts b/src/api/providers/fetchers/kimi-code.ts new file mode 100644 index 0000000000..10b93e26cf --- /dev/null +++ b/src/api/providers/fetchers/kimi-code.ts @@ -0,0 +1,58 @@ +import { z } from "zod" + +import { + KIMI_CODE_BASE_URL, + kimiCodeDefaultModelInfo, + kimiCodeReasoningEfforts, + type ModelInfo, + type ModelRecord, +} from "@roo-code/types" + +const kimiCodeModelSchema = z.object({ + id: z.string().min(1), + context_length: z.number().positive().optional(), + supports_reasoning: z.boolean().optional(), + supports_image_in: z.boolean().optional(), + display_name: z.string().optional(), +}) + +const kimiCodeModelsResponseSchema = z.object({ data: z.array(kimiCodeModelSchema) }) + +const KIMI_CODE_MODELS_TIMEOUT_MS = 10_000 + +export function mapKimiCodeModel(model: z.infer): ModelInfo { + const supportsReasoning = model.supports_reasoning ?? false + return { + ...kimiCodeDefaultModelInfo, + contextWindow: model.context_length ?? kimiCodeDefaultModelInfo.contextWindow, + supportsReasoningEffort: supportsReasoning ? [...kimiCodeReasoningEfforts] : false, + requiredReasoningEffort: supportsReasoning, + reasoningEffort: supportsReasoning ? "max" : undefined, + supportsImages: model.supports_image_in ?? false, + displayName: model.display_name, + } +} + +export async function getKimiCodeModels(apiKey?: string): Promise { + if (!apiKey) throw new Error("Kimi Code authentication is required to fetch models") + const controller = new AbortController() + const timeout = setTimeout( + () => controller.abort(new Error("Kimi Code models request timed out")), + KIMI_CODE_MODELS_TIMEOUT_MS, + ) + try { + const response = await fetch(`${KIMI_CODE_BASE_URL}/models`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + signal: controller.signal, + }) + if (!response.ok) { + const error = new Error(`Kimi Code models request failed: ${response.status} ${response.statusText}`) + ;(error as Error & { status?: number }).status = response.status + throw error + } + const parsed = kimiCodeModelsResponseSchema.parse(await response.json()) + return Object.fromEntries(parsed.data.map((model) => [model.id, mapKimiCodeModel(model)])) + } finally { + clearTimeout(timeout) + } +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index c9e7784949..f56ddba34f 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -31,6 +31,7 @@ import { getPoeModels } from "./poe" import { getDeepSeekModels } from "./deepseek" import { getMoonshotModels } from "./moonshot" import { getZooGatewayModels } from "./zoo-gateway" +import { getKimiCodeModels } from "./kimi-code" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -46,7 +47,7 @@ const inFlightRefresh = new Map>() // allowlists or org policies). For these we MUST NOT cache results on disk or // in memory: a sign-in/out cycle could otherwise serve a previous user's model // list to the next user, and stale data could mask backend allowlist updates. -const AUTH_SCOPED_PROVIDERS: ReadonlySet = new Set(["zoo-gateway"]) +const AUTH_SCOPED_PROVIDERS: ReadonlySet = new Set(["zoo-gateway", "kimi-code"]) // Providers whose model list is determined by the server URL, not just by the provider name. // Each unique baseUrl must be cached independently so that switching endpoints never serves @@ -230,6 +231,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + if ((this.kimiOptions.kimiCodeAuthMethod ?? "oauth") === "api-key") { + if (!this.kimiOptions.kimiCodeApiKey) throw new Error("Kimi Code API key is required") + return this.kimiOptions.kimiCodeApiKey + } + + const token = forceRefresh + ? await kimiCodeOAuthManager.forceRefreshAccessToken() + : await kimiCodeOAuthManager.getAccessToken() + if (!token) throw new Error("Not authenticated with Kimi Code. Sign in from provider settings.") + return token + } + + private async prepareRequest(forceRefresh = false): Promise { + const accessToken = await this.resolveAccessToken(forceRefresh) + this.client.apiKey = accessToken + if (!this.modelDiscoveryAttempted) { + this.modelDiscoveryAttempted = true + try { + this.models = await getModels({ provider: "kimi-code", apiKey: accessToken }) + } catch (error) { + // Model discovery is best-effort; preserve the configured ID and fallback metadata. + console.debug("[KimiCode] Model discovery failed; using fallback model metadata", { + message: error instanceof Error ? error.message : String(error), + }) + } + } + } + + private canRefreshOAuth(): boolean { + return (this.kimiOptions.kimiCodeAuthMethod ?? "oauth") === "oauth" + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + await this.prepareRequest() + try { + yield* super.createMessage(systemPrompt, messages, metadata) + } catch (error) { + if (getHttpStatus(error) !== 401 || !this.canRefreshOAuth()) throw error + await this.prepareRequest(true) + yield* super.createMessage(systemPrompt, messages, metadata) + } + } + + override async completePrompt(prompt: string): Promise { + await this.prepareRequest() + try { + return await super.completePrompt(prompt) + } catch (error) { + if (getHttpStatus(error) !== 401 || !this.canRefreshOAuth()) throw error + await this.prepareRequest(true) + return super.completePrompt(prompt) + } + } + + override getModel() { + const id = this.kimiOptions.apiModelId || kimiCodeDefaultModelId + const info: ModelInfo = this.models[id] ?? kimiCodeDefaultModelInfo + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.kimiOptions, + defaultTemperature: 0, + }) + return { id, info, ...params } + } +} diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index ca0305aab7..9545068794 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -323,7 +323,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl return response.choices?.[0]?.message.content || "" } catch (error) { if (error instanceof Error) { - throw new Error(`${this.providerName} completion error: ${error.message}`) + const wrapped = new Error(`${this.providerName} completion error: ${error.message}`, { cause: error }) + const source = error as Error & { status?: number; errorDetails?: unknown; code?: unknown } + const target = wrapped as Error & { status?: number; errorDetails?: unknown; code?: unknown } + if (source.status !== undefined) target.status = source.status + if (source.errorDetails !== undefined) target.errorDetails = source.errorDetails + if (source.code !== undefined) target.code = source.code + throw wrapped } throw error diff --git a/src/api/transform/__tests__/reasoning.spec.ts b/src/api/transform/__tests__/reasoning.spec.ts index 15e531b0b7..a14fffe550 100644 --- a/src/api/transform/__tests__/reasoning.spec.ts +++ b/src/api/transform/__tests__/reasoning.spec.ts @@ -640,6 +640,57 @@ describe("reasoning.ts", () => { expect(result).toBeUndefined() }) + + it("should fall back to the model default when the persisted effort is outside the capability array", () => { + const modelWithEffortArray: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high", "max"], + reasoningEffort: "max", + } + + const options = { + ...baseOptions, + model: modelWithEffortArray, + settings: { reasoningEffort: "medium" } as ProviderSettings, + reasoningEffort: undefined, + } + + expect(getOpenAiReasoning(options)).toEqual({ reasoning_effort: "max" }) + }) + + it("should not fall back to the model default when reasoning was explicitly disabled", () => { + const modelWithEffortArray: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high", "max"], + reasoningEffort: "max", + } + + const options = { + ...baseOptions, + model: modelWithEffortArray, + settings: { enableReasoningEffort: false, reasoningEffort: "disable" } as ProviderSettings, + reasoningEffort: undefined, + } + + expect(getOpenAiReasoning(options)).toBeUndefined() + }) + + it("should not fall back to the model default when the persisted effort is an explicit none", () => { + const modelWithEffortArray: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high", "max"], + reasoningEffort: "max", + } + + const options = { + ...baseOptions, + model: modelWithEffortArray, + settings: { reasoningEffort: "none" } as ProviderSettings, + reasoningEffort: undefined, + } + + expect(getOpenAiReasoning(options)).toBeUndefined() + }) }) describe("Gemini reasoning (effort models)", () => { diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index 37a19bec55..c51111125a 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -129,6 +129,25 @@ export const getOpenAiReasoning = ({ reasoningEffort, settings, }: GetModelReasoningOptions): OpenAiReasoningParams | undefined => { + // A persisted effort from another provider may fall outside this model's + // capability array; fall back to the model's default effort (mirrors the + // Gemini path) so the request still carries a valid value. + const capability = model.supportsReasoningEffort + if ( + settings?.enableReasoningEffort !== false && + Array.isArray(capability) && + model.reasoningEffort && + (capability as ReadonlyArray).includes(model.reasoningEffort) && + settings?.reasoningEffort && + settings.reasoningEffort !== "disable" && + settings.reasoningEffort !== "none" && + !(capability as ReadonlyArray).includes(settings.reasoningEffort) + ) { + return { + reasoning_effort: model.reasoningEffort as OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"], + } + } + if (!shouldUseReasoningEffort({ model, settings })) return undefined if (reasoningEffort === "disable" || !reasoningEffort) return undefined diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 613a5c1c1d..68a440a6f5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2559,6 +2559,22 @@ export class ClineProvider return false } })(), + kimiCodeIsAuthenticated: await (async () => { + try { + const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") + return await kimiCodeOAuthManager.isAuthenticated() + } catch { + return false + } + })(), + kimiCodeOAuthState: await (async () => { + try { + const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") + return kimiCodeOAuthManager.getState() + } catch { + return undefined + } + })(), ...zooCodeState, platform: process.platform, arch: process.arch, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 7af3752b0d..dbb4fe52d8 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2706,6 +2706,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) @@ -2759,6 +2760,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) @@ -2858,6 +2860,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index de6bb54c57..26aeeb2f1d 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -404,6 +404,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) @@ -591,6 +592,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) @@ -652,6 +654,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "kimi-code": {}, }, values: undefined, }) @@ -1470,3 +1473,148 @@ describe("zooCodeSignOut", () => { ) }) }) + +describe("webviewMessageHandler - kimiCodeSignIn", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + }) + + it("starts OAuth authorization and opens browser", async () => { + const mockStartAuthorization = vi.fn().mockResolvedValue({ + userCode: "TEST-CODE", + verificationUri: "https://auth.kimi.com/device", + expiresAt: Date.now() + 600000, + }) + const mockWaitForAuthorization = vi.fn().mockResolvedValue({ + type: "kimi-code", + accessToken: "token", + refreshToken: "refresh", + expiresAt: Date.now() + 3600000, + }) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + startAuthorization: mockStartAuthorization, + waitForAuthorization: mockWaitForAuthorization, + }, + })) + + const mockOpenExternal = vi.fn().mockResolvedValue(true) + ;(vscode as any).env = { openExternal: mockOpenExternal } + ;(vscode as any).Uri = { parse: vi.fn((url: string) => url) } + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignIn" }) + + expect(mockStartAuthorization).toHaveBeenCalled() + expect(mockOpenExternal).toHaveBeenCalled() + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("shows success message after successful authorization", async () => { + const mockStartAuthorization = vi.fn().mockResolvedValue({ + userCode: "TEST-CODE", + verificationUri: "https://auth.kimi.com/device", + expiresAt: Date.now() + 600000, + }) + const mockWaitForAuthorization = vi.fn().mockResolvedValue({ + type: "kimi-code", + accessToken: "token", + refreshToken: "refresh", + expiresAt: Date.now() + 3600000, + }) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + startAuthorization: mockStartAuthorization, + waitForAuthorization: mockWaitForAuthorization, + }, + })) + + const mockOpenExternal = vi.fn().mockResolvedValue(true) + ;(vscode as any).env = { openExternal: mockOpenExternal } + ;(vscode as any).Uri = { parse: vi.fn((url: string) => url) } + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignIn" }) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("Successfully signed in to Kimi Code") + }) + + it("handles authorization failure", async () => { + const mockStartAuthorization = vi.fn().mockResolvedValue({ + userCode: "TEST-CODE", + verificationUri: "https://auth.kimi.com/device", + expiresAt: Date.now() + 600000, + }) + const mockWaitForAuthorization = vi.fn().mockRejectedValue(new Error("Authorization cancelled")) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + startAuthorization: mockStartAuthorization, + waitForAuthorization: mockWaitForAuthorization, + }, + })) + + const mockOpenExternal = vi.fn().mockResolvedValue(true) + ;(vscode as any).env = { openExternal: mockOpenExternal } + ;(vscode as any).Uri = { parse: vi.fn((url: string) => url) } + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignIn" }) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("handles startAuthorization error", async () => { + const mockStartAuthorization = vi.fn().mockRejectedValue(new Error("Network error")) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + startAuthorization: mockStartAuthorization, + }, + })) + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignIn" }) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(expect.stringContaining("Kimi Code sign in failed")) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) +}) + +describe("webviewMessageHandler - kimiCodeSignOut", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + }) + + it("clears credentials and shows success message", async () => { + const mockClearCredentials = vi.fn().mockResolvedValue(undefined) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + clearCredentials: mockClearCredentials, + }, + })) + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignOut" }) + + expect(mockClearCredentials).toHaveBeenCalled() + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("Signed out from Kimi Code") + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("handles sign out error", async () => { + const mockClearCredentials = vi.fn().mockRejectedValue(new Error("Clear failed")) + + vi.doMock("../../../integrations/kimi-code/oauth", () => ({ + kimiCodeOAuthManager: { + clearCredentials: mockClearCredentials, + }, + })) + + await webviewMessageHandler(mockClineProvider, { type: "kimiCodeSignOut" }) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Kimi Code sign out failed.") + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index dd15879c1c..02f8602b5a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1053,6 +1053,7 @@ export const webviewMessageHandler = async ( moonshot: {}, "opencode-go": {}, kenari: {}, + "kimi-code": {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1195,6 +1196,22 @@ export const webviewMessageHandler = async ( options: { provider: "kenari", apiKey: kenariApiKey }, }) + if (!providerFilter || providerFilter === "kimi-code") { + const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") + const kimiCodeAuthMethod = + message?.values?.kimiCodeAuthMethod ?? apiConfiguration.kimiCodeAuthMethod ?? "oauth" + const kimiCodeApiKey = + kimiCodeAuthMethod === "api-key" + ? (message?.values?.kimiCodeApiKey ?? apiConfiguration.kimiCodeApiKey) + : await kimiCodeOAuthManager.getAccessToken() + if (kimiCodeApiKey) { + candidates.push({ + key: "kimi-code", + options: { provider: "kimi-code", apiKey: kimiCodeApiKey }, + }) + } + } + // Apply single provider filter if specified const modelFetchPromises = providerFilter ? candidates.filter(({ key }) => key === providerFilter) @@ -2626,6 +2643,45 @@ export const webviewMessageHandler = async ( } break } + case "kimiCodeSignIn": { + try { + const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") + const device = await kimiCodeOAuthManager.startAuthorization() + await provider.postStateToWebview() + await vscode.env.openExternal( + vscode.Uri.parse(device.verificationUriComplete ?? device.verificationUri), + ) + void kimiCodeOAuthManager + .waitForAuthorization() + .then(async () => { + vscode.window.showInformationMessage("Successfully signed in to Kimi Code") + await provider.postStateToWebview() + }) + .catch(async (error) => { + provider.log(`Kimi Code OAuth failed: ${error}`) + await provider.postStateToWebview() + }) + } catch (error) { + provider.log(`Kimi Code OAuth failed: ${error}`) + vscode.window.showErrorMessage( + `Kimi Code sign in failed: ${error instanceof Error ? error.message : error}`, + ) + await provider.postStateToWebview() + } + break + } + case "kimiCodeSignOut": { + try { + const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") + await kimiCodeOAuthManager.clearCredentials() + vscode.window.showInformationMessage("Signed out from Kimi Code") + await provider.postStateToWebview() + } catch (error) { + provider.log(`Kimi Code sign out failed: ${error}`) + vscode.window.showErrorMessage("Kimi Code sign out failed.") + } + break + } case "rooCloudManualUrl": { if (!isCloudServiceAvailable()) { provider.log("CloudService unavailable; ignoring rooCloudManualUrl") diff --git a/src/extension.ts b/src/extension.ts index f55aa61405..f006b4b52b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,6 +32,7 @@ import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" import { Terminal } from "./integrations/terminal/Terminal" import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry" import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth" +import { kimiCodeOAuthManager } from "./integrations/kimi-code/oauth" import { McpServerManager } from "./services/mcp/McpServerManager" import { CodeIndexManager } from "./services/code-index/manager" import { MdmService } from "./services/mdm/MdmService" @@ -157,6 +158,8 @@ export async function activate(context: vscode.ExtensionContext) { // Initialize OpenAI Codex OAuth manager for ChatGPT subscription-based access. openAiCodexOAuthManager.initialize(context, (message) => outputChannel.appendLine(message)) + // Kimi Code OAuth tokens live only in VS Code SecretStorage, outside provider profile JSON/cloud sync. + kimiCodeOAuthManager.initialize(context) // Initialize Zoo Code auth service for extension session token management. await initZooCodeAuth(context) diff --git a/src/integrations/kimi-code/__tests__/oauth.spec.ts b/src/integrations/kimi-code/__tests__/oauth.spec.ts new file mode 100644 index 0000000000..960777e69c --- /dev/null +++ b/src/integrations/kimi-code/__tests__/oauth.spec.ts @@ -0,0 +1,465 @@ +import { KIMI_CODE_OAUTH_CONFIG, KimiCodeOAuthManager } from "../oauth" + +const createContext = () => { + const values = new Map() + return { + values, + context: { + secrets: { + get: vi.fn(async (key: string) => values.get(key)), + store: vi.fn(async (key: string, value: string) => void values.set(key, value)), + delete: vi.fn(async (key: string) => void values.delete(key)), + }, + } as any, + } +} + +describe("KimiCodeOAuthManager", () => { + beforeEach(() => vi.restoreAllMocks()) + afterEach(() => vi.useRealTimers()) + + it("uses the official public client ID and form-encoded device request", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "device", + user_code: "ABCD-EFGH", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 60, + }), + { status: 200 }, + ), + ) + + const authorization = await manager.startAuthorization() + expect(authorization.userCode).toBe("ABCD-EFGH") + expect(fetch).toHaveBeenCalledWith( + KIMI_CODE_OAUTH_CONFIG.deviceAuthorizationEndpoint, + expect.objectContaining({ body: `client_id=${KIMI_CODE_OAUTH_CONFIG.clientId}` }), + ) + const cancelledPolling = manager.waitForAuthorization().catch((error) => error) + manager.cancelAuthorization() + await expect(cancelledPolling).resolves.toMatchObject({ message: "Kimi Code authorization was cancelled" }) + }) + + it("deduplicates concurrent access-token refreshes and stores refreshed credentials", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "refresh", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ access_token: "new", refresh_token: "rotated", expires_in: 3600 }), { + status: 200, + }), + ) + + const [first, second] = await Promise.all([manager.getAccessToken(), manager.getAccessToken()]) + expect(first).toBe("new") + expect(second).toBe("new") + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(context.secrets.store).toHaveBeenCalledTimes(1) + }) + + it("returns null when not authenticated", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + expect(await manager.isAuthenticated()).toBe(false) + expect(await manager.getAccessToken()).toBeNull() + }) + + it("returns cached access token when not expired", async () => { + const { context, values } = createContext() + const futureExpiry = Date.now() + 3600_000 + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ + type: "kimi-code", + accessToken: "valid", + refreshToken: "refresh", + expiresAt: futureExpiry, + }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch") + expect(await manager.getAccessToken()).toBe("valid") + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("clears credentials and cancels authorization", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "token", refreshToken: "refresh", expiresAt: 99999 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + await manager.clearCredentials() + expect(values.has("kimi-code-oauth-credentials")).toBe(false) + expect(await manager.isAuthenticated()).toBe(false) + }) + + it("does not restore credentials when sign-out races an in-flight refresh", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "refresh", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + let resolveRefresh!: (response: Response) => void + vi.spyOn(globalThis, "fetch").mockReturnValueOnce( + new Promise((resolve) => { + resolveRefresh = resolve + }), + ) + + const tokenPromise = manager.getAccessToken() + await vi.waitFor(() => expect(fetch).toHaveBeenCalledOnce()) + const clearPromise = manager.clearCredentials() + resolveRefresh( + new Response(JSON.stringify({ access_token: "new", refresh_token: "rotated", expires_in: 3600 }), { + status: 200, + }), + ) + + await expect(tokenPromise).resolves.toBeNull() + await clearPromise + expect(context.secrets.store).not.toHaveBeenCalled() + expect(values.has("kimi-code-oauth-credentials")).toBe(false) + }) + + it("bounds token refresh requests with a deadline", async () => { + vi.useFakeTimers() + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "refresh", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockImplementation((_input, init) => { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }) + }) + }) + const tokenPromise = manager.getAccessToken() + + await vi.advanceTimersByTimeAsync(30_000) + await expect(tokenPromise).resolves.toBeNull() + expect(vi.mocked(fetch).mock.calls[0][1]?.signal?.aborted).toBe(true) + expect(values.has("kimi-code-oauth-credentials")).toBe(true) + expect(vi.getTimerCount()).toBe(0) + }) + + it("keeps the newer polling lifecycle when a prior poll finishes cancellation", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "first", + user_code: "FIRST", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 60, + }), + { status: 200 }, + ), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "second", + user_code: "SECOND", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 60, + }), + { status: 200 }, + ), + ) + + await manager.startAuthorization() + const firstPolling = manager.waitForAuthorization().catch((error) => error) + await manager.startAuthorization() + const secondPolling = manager.waitForAuthorization().catch((error) => error) + await expect(firstPolling).resolves.toMatchObject({ message: "Kimi Code authorization was cancelled" }) + + expect(manager.getState()).toMatchObject({ status: "polling", userCode: "SECOND" }) + expect(() => manager.waitForAuthorization()).not.toThrow() + manager.cancelAuthorization() + await secondPolling + }) + + it("ignores a superseded device request failure", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + let rejectFirst!: (error: Error) => void + vi.spyOn(globalThis, "fetch") + .mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectFirst = reject + }), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "second", + user_code: "SECOND", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 60, + }), + { status: 200 }, + ), + ) + + const first = manager.startAuthorization() + await vi.waitFor(() => expect(fetch).toHaveBeenCalledOnce()) + await manager.startAuthorization() + rejectFirst(new Error("stale request failed")) + await expect(first).rejects.toThrow("stale request failed") + + expect(manager.getState()).toMatchObject({ status: "polling", userCode: "SECOND" }) + expect(() => manager.waitForAuthorization()).not.toThrow() + const secondPolling = manager.waitForAuthorization().catch((error) => error) + manager.cancelAuthorization() + await secondPolling + }) + + it("handles polling completion successfully", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch") + fetchSpy.mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "device", + user_code: "CODE", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 1, + }), + { status: 200 }, + ), + ) + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify({ error: "authorization_pending" }), { status: 400 }), + ) + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: "new", refresh_token: "refresh", expires_in: 3600 }), { + status: 200, + }), + ) + + await manager.startAuthorization() + const credentials = await manager.waitForAuthorization() + expect(credentials.accessToken).toBe("new") + expect(await manager.isAuthenticated()).toBe(true) + }) + + it("handles slow_down error during polling", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch") + fetchSpy.mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "device", + user_code: "CODE", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 1, + }), + { status: 200 }, + ), + ) + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ error: "slow_down" }), { status: 400 })) + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: "new", refresh_token: "refresh", expires_in: 3600 }), { + status: 200, + }), + ) + + await manager.startAuthorization() + const credentials = await manager.waitForAuthorization() + expect(credentials.accessToken).toBe("new") + }) + + it("handles authorization expiration", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch") + fetchSpy.mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "device", + user_code: "CODE", + verification_uri: "https://auth.kimi.com/device", + expires_in: 1, + interval: 1, + }), + { status: 200 }, + ), + ) + fetchSpy.mockResolvedValue(new Response(JSON.stringify({ error: "authorization_pending" }), { status: 400 })) + + vi.spyOn(Date, "now").mockReturnValueOnce(0).mockReturnValue(2000) + + await manager.startAuthorization() + await expect(manager.waitForAuthorization()).rejects.toThrow("Kimi Code authorization expired") + }) + + it("handles device authorization errors", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ error: "invalid_request", error_description: "Bad request" }), { + status: 400, + }), + ) + + await expect(manager.startAuthorization()).rejects.toThrow("Bad request") + }) + + it("handles token refresh failure with invalid_grant", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "invalid", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 }), + ) + + const token = await manager.getAccessToken() + expect(token).toBeNull() + expect(values.has("kimi-code-oauth-credentials")).toBe(false) + }) + + it("handles refresh errors that preserve state", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "refresh", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ error: "server_error" }), { status: 500 }), + ) + + const token = await manager.getAccessToken() + expect(token).toBeNull() + expect(values.has("kimi-code-oauth-credentials")).toBe(true) + }) + + it("forces refresh even when token is not expired", async () => { + const { context, values } = createContext() + const futureExpiry = Date.now() + 3600_000 + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "refresh", expiresAt: futureExpiry }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: "forced", refresh_token: "new_refresh", expires_in: 3600 }), { + status: 200, + }), + ) + + const token = await manager.forceRefreshAccessToken() + expect(token).toBe("forced") + }) + + it("handles malformed OAuth error response", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response("not json", { status: 400 })) + + await expect(manager.startAuthorization()).rejects.toThrow("OAuth request failed") + }) + + it("throws error when no refresh token in device token response", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + const fetchSpy = vi.spyOn(globalThis, "fetch") + fetchSpy.mockResolvedValueOnce( + new Response( + JSON.stringify({ + device_code: "device", + user_code: "CODE", + verification_uri: "https://auth.kimi.com/device", + expires_in: 600, + interval: 1, + }), + { status: 200 }, + ), + ) + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: "token", expires_in: 3600 }), { status: 200 }), + ) + + await manager.startAuthorization() + await expect(manager.waitForAuthorization()).rejects.toThrow("did not return a refresh token") + }) + + it("returns state correctly", async () => { + const { context } = createContext() + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + expect(manager.getState().status).toBe("idle") + }) + + it("throws when waitForAuthorization called without active authorization", () => { + const manager = new KimiCodeOAuthManager() + expect(() => manager.waitForAuthorization()).toThrow("No Kimi Code authorization is in progress") + }) + + it("handles invalid stored credentials", async () => { + const { context, values } = createContext() + values.set("kimi-code-oauth-credentials", "invalid json") + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + expect(await manager.getAccessToken()).toBeNull() + expect(values.has("kimi-code-oauth-credentials")).toBe(false) + }) + + it("preserves refresh token when not rotated", async () => { + const { context, values } = createContext() + values.set( + "kimi-code-oauth-credentials", + JSON.stringify({ type: "kimi-code", accessToken: "old", refreshToken: "keep", expiresAt: 0 }), + ) + const manager = new KimiCodeOAuthManager() + manager.initialize(context) + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: "new", expires_in: 3600 }), { status: 200 }), + ) + + await manager.getAccessToken() + const stored = JSON.parse(values.get("kimi-code-oauth-credentials")!) + expect(stored.refreshToken).toBe("keep") + }) +}) diff --git a/src/integrations/kimi-code/oauth.ts b/src/integrations/kimi-code/oauth.ts new file mode 100644 index 0000000000..cd28df8b04 --- /dev/null +++ b/src/integrations/kimi-code/oauth.ts @@ -0,0 +1,379 @@ +import type { ExtensionContext } from "vscode" +import { z } from "zod" + +export const KIMI_CODE_OAUTH_CONFIG = { + authHost: "https://auth.kimi.com", + deviceAuthorizationEndpoint: "https://auth.kimi.com/api/oauth/device_authorization", + tokenEndpoint: "https://auth.kimi.com/api/oauth/token", + deviceGrantType: "urn:ietf:params:oauth:grant-type:device_code", + // Kimi Code's official public client ID for the OAuth device flow. + // Source: Kimi Code CLI's published OAuth integration. + clientId: "17e5f671-d194-4dfb-9706-5516cb48c098", +} as const + +const KIMI_CODE_CREDENTIALS_KEY = "kimi-code-oauth-credentials" +const TOKEN_EXPIRY_BUFFER_MS = 60_000 +const OAUTH_REQUEST_TIMEOUT_MS = 30_000 + +const credentialsSchema = z.object({ + type: z.literal("kimi-code"), + accessToken: z.string().min(1), + refreshToken: z.string().min(1), + expiresAt: z.number(), + tokenType: z.string().optional(), +}) + +const deviceAuthorizationSchema = z.object({ + device_code: z.string().min(1), + user_code: z.string().min(1), + verification_uri: z.string().url(), + verification_uri_complete: z.string().url().optional(), + expires_in: z.number().positive(), + interval: z.number().positive().optional(), +}) + +const tokenResponseSchema = z.object({ + access_token: z.string().min(1), + refresh_token: z.string().min(1).optional(), + expires_in: z.number().positive(), + token_type: z.string().optional(), +}) + +const oauthErrorSchema = z.object({ + error: z.string(), + error_description: z.string().optional(), +}) + +export type KimiCodeCredentials = z.infer + +export type KimiCodeOAuthState = { + status: "idle" | "authorizing" | "polling" | "authenticated" | "error" + userCode?: string + verificationUri?: string + expiresAt?: number + error?: string +} + +export type KimiCodeDeviceAuthorization = { + userCode: string + verificationUri: string + verificationUriComplete?: string + expiresAt: number +} + +class KimiCodeOAuthError extends Error { + constructor( + message: string, + public readonly code?: string, + ) { + super(message) + this.name = "KimiCodeOAuthError" + } +} + +async function postForm(endpoint: string, values: Record, signal?: AbortSignal): Promise { + const controller = new AbortController() + const forwardAbort = () => controller.abort(signal?.reason) + if (signal?.aborted) forwardAbort() + else signal?.addEventListener("abort", forwardAbort, { once: true }) + const timeout = setTimeout( + () => controller.abort(new Error("Kimi Code OAuth request timed out")), + OAUTH_REQUEST_TIMEOUT_MS, + ) + try { + return await fetch(endpoint, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(values).toString(), + signal: controller.signal, + }) + } finally { + clearTimeout(timeout) + signal?.removeEventListener("abort", forwardAbort) + } +} + +async function readOAuthError(response: Response): Promise { + const text = await response.text() + try { + const parsed = oauthErrorSchema.parse(JSON.parse(text)) + return new KimiCodeOAuthError(parsed.error_description ?? parsed.error, parsed.error) + } catch { + return new KimiCodeOAuthError(`OAuth request failed: ${response.status} ${response.statusText} - ${text}`) + } +} + +export async function requestDeviceAuthorization(signal?: AbortSignal) { + const response = await postForm( + KIMI_CODE_OAUTH_CONFIG.deviceAuthorizationEndpoint, + { client_id: KIMI_CODE_OAUTH_CONFIG.clientId }, + signal, + ) + if (!response.ok) throw await readOAuthError(response) + return deviceAuthorizationSchema.parse(await response.json()) +} + +async function requestDeviceToken(deviceCode: string, signal?: AbortSignal): Promise { + const response = await postForm( + KIMI_CODE_OAUTH_CONFIG.tokenEndpoint, + { + grant_type: KIMI_CODE_OAUTH_CONFIG.deviceGrantType, + device_code: deviceCode, + client_id: KIMI_CODE_OAUTH_CONFIG.clientId, + }, + signal, + ) + if (!response.ok) throw await readOAuthError(response) + const tokens = tokenResponseSchema.parse(await response.json()) + if (!tokens.refresh_token) throw new Error("Kimi Code OAuth did not return a refresh token") + return { + type: "kimi-code", + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt: Date.now() + tokens.expires_in * 1000, + tokenType: tokens.token_type, + } +} + +export async function refreshKimiCodeAccessToken(credentials: KimiCodeCredentials): Promise { + const response = await postForm(KIMI_CODE_OAUTH_CONFIG.tokenEndpoint, { + grant_type: "refresh_token", + refresh_token: credentials.refreshToken, + client_id: KIMI_CODE_OAUTH_CONFIG.clientId, + }) + if (!response.ok) throw await readOAuthError(response) + const tokens = tokenResponseSchema.parse(await response.json()) + return { + type: "kimi-code", + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token ?? credentials.refreshToken, + expiresAt: Date.now() + tokens.expires_in * 1000, + tokenType: tokens.token_type ?? credentials.tokenType, + } +} + +const delay = (milliseconds: number, signal: AbortSignal) => + new Promise((resolve, reject) => { + const timeout = signal.aborted + ? undefined + : setTimeout(() => { + signal.removeEventListener("abort", onAbort) + resolve() + }, milliseconds) + const onAbort = () => { + if (timeout) clearTimeout(timeout) + reject(new Error("Kimi Code authorization was cancelled")) + } + if (signal.aborted) { + onAbort() + return + } + signal.addEventListener("abort", onAbort, { once: true }) + }) + +export class KimiCodeOAuthManager { + private context: ExtensionContext | null = null + private credentials: KimiCodeCredentials | null = null + private state: KimiCodeOAuthState = { status: "idle" } + private refreshPromise: Promise | null = null + private pollingPromise: Promise | null = null + private pollingController: AbortController | null = null + private credentialsGeneration = 0 + private authorizationGeneration = 0 + + initialize(context: ExtensionContext): void { + this.context = context + } + + getState(): KimiCodeOAuthState { + return { ...this.state } + } + + private async loadCredentials(): Promise { + if (this.credentials) return this.credentials + const stored = await this.context?.secrets.get(KIMI_CODE_CREDENTIALS_KEY) + if (!stored) return null + try { + this.credentials = credentialsSchema.parse(JSON.parse(stored)) + return this.credentials + } catch { + await this.context?.secrets.delete(KIMI_CODE_CREDENTIALS_KEY) + return null + } + } + + private async saveCredentials( + credentials: KimiCodeCredentials, + isCurrent: () => boolean = () => true, + ): Promise { + if (!this.context) throw new Error("Kimi Code OAuth manager is not initialized") + if (!isCurrent()) return false + const serialized = JSON.stringify(credentials) + await this.context.secrets.store(KIMI_CODE_CREDENTIALS_KEY, serialized) + if (!isCurrent()) { + if ((await this.context.secrets.get(KIMI_CODE_CREDENTIALS_KEY)) === serialized) { + await this.context.secrets.delete(KIMI_CODE_CREDENTIALS_KEY) + } + return false + } + this.credentials = credentials + this.state = { status: "authenticated" } + return true + } + + async clearCredentials(): Promise { + this.credentialsGeneration++ + this.cancelAuthorization() + const refreshPromise = this.refreshPromise + if (refreshPromise) await refreshPromise.catch(() => null) + await this.context?.secrets.delete(KIMI_CODE_CREDENTIALS_KEY) + this.credentials = null + this.state = { status: "idle" } + } + + async isAuthenticated(): Promise { + return (await this.getAccessToken()) !== null + } + + async getAccessToken(forceRefresh = false): Promise { + const credentials = await this.loadCredentials() + if (!credentials) return null + if (!forceRefresh && Date.now() < credentials.expiresAt - TOKEN_EXPIRY_BUFFER_MS) { + return credentials.accessToken + } + if (!this.refreshPromise) { + const generation = this.credentialsGeneration + this.refreshPromise = refreshKimiCodeAccessToken(credentials) + .then(async (next) => { + if (!(await this.saveCredentials(next, () => generation === this.credentialsGeneration))) + return null + return next + }) + .catch(async (error) => { + if (error instanceof KimiCodeOAuthError && error.code === "invalid_grant") { + this.credentialsGeneration++ + await this.context?.secrets.delete(KIMI_CODE_CREDENTIALS_KEY) + this.credentials = null + this.state = { status: "idle" } + } + return null + }) + .finally(() => { + this.refreshPromise = null + }) + } + return (await this.refreshPromise)?.accessToken ?? null + } + + async forceRefreshAccessToken(): Promise { + return this.getAccessToken(true) + } + + async startAuthorization(): Promise { + this.cancelAuthorization() + const generation = ++this.authorizationGeneration + this.state = { status: "authorizing" } + const controller = new AbortController() + this.pollingController = controller + try { + const device = await requestDeviceAuthorization(controller.signal) + if (generation !== this.authorizationGeneration || this.pollingController !== controller) { + throw new Error("Kimi Code authorization was cancelled") + } + const expiresAt = Date.now() + device.expires_in * 1000 + const verificationUri = device.verification_uri_complete ?? device.verification_uri + this.state = { + status: "polling", + userCode: device.user_code, + verificationUri, + expiresAt, + } + this.pollingPromise = this.pollForToken( + device.device_code, + expiresAt, + device.interval ?? 5, + controller, + generation, + ) + return { + userCode: device.user_code, + verificationUri: device.verification_uri, + verificationUriComplete: device.verification_uri_complete, + expiresAt, + } + } catch (error) { + if (generation === this.authorizationGeneration && this.pollingController === controller) { + this.state = { status: "error", error: error instanceof Error ? error.message : String(error) } + this.pollingController = null + } + throw error + } + } + + waitForAuthorization(): Promise { + if (!this.pollingPromise) throw new Error("No Kimi Code authorization is in progress") + return this.pollingPromise + } + + cancelAuthorization(): void { + this.authorizationGeneration++ + this.pollingController?.abort() + this.pollingController = null + this.pollingPromise = null + if (this.state.status === "authorizing" || this.state.status === "polling") this.state = { status: "idle" } + } + + private async pollForToken( + deviceCode: string, + expiresAt: number, + initialIntervalSeconds: number, + controller: AbortController, + generation: number, + ): Promise { + let intervalSeconds = initialIntervalSeconds + try { + while (Date.now() < expiresAt) { + await delay(intervalSeconds * 1000, controller.signal) + try { + const credentials = await requestDeviceToken(deviceCode, controller.signal) + if (generation !== this.authorizationGeneration || this.pollingController !== controller) { + throw new Error("Kimi Code authorization was cancelled") + } + const saved = await this.saveCredentials( + credentials, + () => generation === this.authorizationGeneration && this.pollingController === controller, + ) + if (!saved) throw new Error("Kimi Code authorization was cancelled") + return credentials + } catch (error) { + if (error instanceof KimiCodeOAuthError && error.code === "authorization_pending") continue + if (error instanceof KimiCodeOAuthError && error.code === "slow_down") { + intervalSeconds += 5 + continue + } + throw error + } + } + throw new Error("Kimi Code authorization expired") + } catch (error) { + if ( + !controller.signal.aborted && + generation === this.authorizationGeneration && + this.pollingController === controller + ) { + this.state = { status: "error", error: error instanceof Error ? error.message : String(error) } + } + throw error + } finally { + if (generation === this.authorizationGeneration && this.pollingController === controller) { + this.pollingController = null + this.pollingPromise = null + } + } + } +} + +export const kimiCodeOAuthManager = new KimiCodeOAuthManager() diff --git a/src/shared/__tests__/checkExistApiConfig.spec.ts b/src/shared/__tests__/checkExistApiConfig.spec.ts index 467fbcc45d..5c32e567e2 100644 --- a/src/shared/__tests__/checkExistApiConfig.spec.ts +++ b/src/shared/__tests__/checkExistApiConfig.spec.ts @@ -87,6 +87,38 @@ describe("checkExistKey", () => { expect(checkExistKey(config)).toBe(false) }) + it("should return true for kimi-code provider with OAuth auth method", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "oauth", + } + expect(checkExistKey(config)).toBe(true) + }) + + it("should return true for kimi-code provider without auth method (defaults to OAuth)", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + } + expect(checkExistKey(config)).toBe(true) + }) + + it("should return true for kimi-code provider with api-key auth and key present", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "test-key", + } + expect(checkExistKey(config)).toBe(true) + }) + + it("should return false for kimi-code provider with api-key auth but no key", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + } + expect(checkExistKey(config)).toBe(false) + }) + it("should return false for zoo-gateway without session token or auth", () => { const config: ProviderSettings = { apiProvider: "zoo-gateway", diff --git a/src/shared/api.ts b/src/shared/api.ts index 0cec753591..056612f9f9 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -189,6 +189,7 @@ const dynamicProviderExtras = { moonshot: {} as { apiKey?: string; baseUrl?: string }, "opencode-go": {} as { apiKey?: string }, kenari: {} as { apiKey?: string }, + "kimi-code": {} as { apiKey?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 7e5ff2f258..9ce55a9ac3 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -18,6 +18,10 @@ export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAut return true } + if (config.apiProvider === "kimi-code" && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") { + return true + } + // Zoo Gateway uses session auth (profile token and/or global Zoo Code login), // not a traditional API key listed in SECRET_STATE_KEYS. if (config.apiProvider === "zoo-gateway") { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 2585a674ec..d38f49f958 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -56,6 +56,7 @@ import { LiteLLM, Mistral, Moonshot, + KimiCode, Ollama, OpenAI, OpenAICompatible, @@ -116,7 +117,8 @@ const ApiOptions = ({ setErrorMessage, }: ApiOptionsProps) => { const { t } = useAppTranslation() - const { organizationAllowList, openAiCodexIsAuthenticated } = useExtensionState() + const { organizationAllowList, openAiCodexIsAuthenticated, kimiCodeIsAuthenticated, kimiCodeOAuthState } = + useExtensionState() const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { const headers = apiConfiguration?.openAiHeaders || {} @@ -575,6 +577,15 @@ const ApiOptions = ({ /> )} + {selectedProvider === "kimi-code" && ( + + )} + {selectedProvider === "minimax" && ( (field: K, value: ProviderSettings[K]) => void + kimiCodeIsAuthenticated?: boolean + kimiCodeOAuthState?: { + status: "idle" | "authorizing" | "polling" | "authenticated" | "error" + userCode?: string + verificationUri?: string + error?: string + } +} + +export const KimiCode = ({ + apiConfiguration, + setApiConfigurationField, + kimiCodeIsAuthenticated = false, + kimiCodeOAuthState, +}: KimiCodeProps) => { + const { t } = useAppTranslation() + const authMethod = apiConfiguration.kimiCodeAuthMethod ?? "oauth" + const { data, refetch, isFetching } = useRouterModels({ + provider: "kimi-code", + enabled: authMethod === "oauth" ? kimiCodeIsAuthenticated : !!apiConfiguration.kimiCodeApiKey, + }) + const discoveredModels = data?.["kimi-code"] + const models: ModelRecord = + discoveredModels && Object.keys(discoveredModels).length > 0 ? discoveredModels : kimiCodeModels + + useEffect(() => { + if (authMethod === "oauth" && kimiCodeIsAuthenticated) void refetch() + }, [authMethod, kimiCodeIsAuthenticated, refetch]) + + const refreshModels = () => { + vscode.postMessage({ + type: "requestRouterModels", + values: { + provider: "kimi-code", + refresh: true, + kimiCodeAuthMethod: authMethod, + kimiCodeApiKey: apiConfiguration.kimiCodeApiKey, + }, + }) + void refetch() + } + + return ( +
+
+ + +
+ + {authMethod === "oauth" ? ( +
+ {kimiCodeIsAuthenticated ? ( +
+ + {t("settings:providers.kimiCode.authenticated")} + + +
+ ) : ( + + )} + {kimiCodeOAuthState?.status === "polling" && ( +
+

+ {t("settings:providers.kimiCode.deviceCodeHelp")} +

+ + {kimiCodeOAuthState.userCode} + + {kimiCodeOAuthState.verificationUri && ( + + {kimiCodeOAuthState.verificationUri} + + )} +
+ )} + {kimiCodeOAuthState?.status === "error" && ( +

{kimiCodeOAuthState.error}

+ )} +
+ ) : ( + + setApiConfigurationField("kimiCodeApiKey", (event.target as HTMLInputElement).value) + } + className="w-full" + data-testid="kimi-code-api-key"> + + + )} + + + + {t("settings:providers.kimiCode.docs")} +
+ ) +} diff --git a/webview-ui/src/components/settings/providers/__tests__/KimiCode.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/KimiCode.spec.tsx new file mode 100644 index 0000000000..76a2bfaddf --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/KimiCode.spec.tsx @@ -0,0 +1,41 @@ +import { fireEvent, render, screen } from "@testing-library/react" + +import { KimiCode } from "../KimiCode" + +vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: () => ({ data: { "kimi-code": {} }, refetch: vi.fn(), isFetching: false }), +})) + +vi.mock("../../ModelPicker", () => ({ + ModelPicker: () =>
, +})) + +describe("KimiCode settings", () => { + it("binds the API key input through the buffered settings setter", () => { + const setField = vi.fn() + render( + , + ) + fireEvent.input(screen.getByTestId("kimi-code-api-key"), { target: { value: "new-key" } }) + expect(setField).toHaveBeenCalledWith("kimiCodeApiKey", "new-key") + expect(screen.getByTestId("kimi-code-model-picker")).toBeInTheDocument() + }) + + it("shows device-code polling state", () => { + render( + , + ) + expect(screen.getByTestId("kimi-code-device-code")).toHaveTextContent("ABCD-EFGH") + }) +}) diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 6a990c37a5..8d725efed2 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -5,6 +5,7 @@ export { Gemini } from "./Gemini" export { LMStudio } from "./LMStudio" export { Mistral } from "./Mistral" export { Moonshot } from "./Moonshot" +export { KimiCode } from "./KimiCode" export { Ollama } from "./Ollama" export { OpenAI } from "./OpenAI" export { OpenAICodex } from "./OpenAICodex" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index ed3df0209d..da660976f4 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -4,6 +4,7 @@ import { bedrockDefaultModelId, deepSeekDefaultModelId, moonshotDefaultModelId, + kimiCodeDefaultModelId, geminiDefaultModelId, mistralDefaultModelId, openRouterDefaultModelId, @@ -42,6 +43,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial> = bedrock: bedrockDefaultModelId, deepseek: deepSeekDefaultModelId, moonshot: moonshotDefaultModelId, + "kimi-code": kimiCodeDefaultModelId, gemini: geminiDefaultModelId, mistral: mistralDefaultModelId, "openai-native": openAiNativeDefaultModelId, @@ -117,6 +120,7 @@ const PROVIDER_MODEL_CONFIG: Partial> gemini: { field: "apiModelId", default: geminiDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, moonshot: { field: "apiModelId", default: moonshotDefaultModelId }, + "kimi-code": { field: "apiModelId", default: kimiCodeDefaultModelId }, minimax: { field: "apiModelId", default: minimaxDefaultModelId }, mimo: { field: "apiModelId", default: mimoDefaultModelId }, mistral: { field: "apiModelId", default: mistralDefaultModelId }, @@ -201,6 +205,7 @@ export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [ "unbound", "openai", // OpenAI Compatible "openai-codex", // OpenAI Codex has custom UI with auth and rate limits + "kimi-code", "litellm", "vercel-ai-gateway", "ollama", diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 93dca94a24..50886d4538 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -26,6 +26,7 @@ import { friendliModels, basetenModels, qwenCodeModels, + kimiCodeDefaultModelInfo, litellmDefaultModelInfo, lMStudioDefaultModelInfo, opencodeGoDefaultModelInfo, @@ -103,7 +104,12 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { lmStudioModels: (lmStudioModels.data || undefined) as ModelRecord | undefined, ollamaModels: (ollamaModels.data || undefined) as ModelRecord | undefined, }) - : { id: getProviderDefaultModelId(activeProvider ?? "openrouter"), info: undefined } + : activeProvider === "kimi-code" && apiConfiguration + ? { + id: apiConfiguration.apiModelId || getProviderDefaultModelId("kimi-code"), + info: kimiCodeDefaultModelInfo, + } + : { id: getProviderDefaultModelId(activeProvider ?? "openrouter"), info: undefined } return { provider, @@ -261,6 +267,12 @@ function getSelectedModel({ const staticInfo = moonshotModels[id as keyof typeof moonshotModels] return { id, info: routerInfo ?? staticInfo } } + case "kimi-code": { + const configuredId = apiConfiguration.apiModelId + const availableModels = routerModels["kimi-code"] + const id = configuredId || defaultModelId + return { id, info: availableModels?.[id] ?? kimiCodeDefaultModelInfo } + } case "minimax": { const id = apiConfiguration.apiModelId ?? defaultModelId const info = minimaxModels[id as keyof typeof minimaxModels] diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 4cb1e90027..f861b8734c 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Clau API de Moonshot", "getMoonshotApiKey": "Obtenir clau API de Moonshot", "moonshotBaseUrl": "Punt d'entrada de Moonshot", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Clau API de Z AI", "getZaiApiKey": "Obtenir clau API de Z AI", "zaiEntrypoint": "Punt d'entrada de Z AI", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 8566410041..e1ff092237 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API-Schlüssel", "getMoonshotApiKey": "Moonshot API-Schlüssel erhalten", "moonshotBaseUrl": "Moonshot-Einstiegspunkt", + "kimiCode": { + "authMethod": "Authentifizierungsmethode", + "oauth": "Kimi Code-Abonnement (OAuth)", + "apiKey": "Kimi Code-API-Schlüssel", + "apiKeyLabel": "Kimi Code-API-Schlüssel", + "signIn": "Bei Kimi Code anmelden", + "signOut": "Abmelden", + "authenticated": "Bei Kimi Code angemeldet", + "deviceCodeHelp": "Gib diesen Gerätecode auf der Kimi-Autorisierungsseite ein:", + "docs": "Kimi Code-Dokumentation" + }, "zaiApiKey": "Z AI API-Schlüssel", "getZaiApiKey": "Z AI API-Schlüssel erhalten", "zaiEntrypoint": "Z AI Einstiegspunkt", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 9ef634f866..37d33f8172 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -539,6 +539,17 @@ "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "minimaxApiKey": "MiniMax API Key", "getMiniMaxApiKey": "Get MiniMax API Key", "minimaxBaseUrl": "MiniMax Entrypoint", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 7500e5bc77..4a5ce59d79 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Clave API de Moonshot", "getMoonshotApiKey": "Obtener clave API de Moonshot", "moonshotBaseUrl": "Punto de entrada de Moonshot", + "kimiCode": { + "authMethod": "Método de autenticación", + "oauth": "Suscripción a Kimi Code (OAuth)", + "apiKey": "Clave de API de Kimi Code", + "apiKeyLabel": "Clave de API de Kimi Code", + "signIn": "Iniciar sesión en Kimi Code", + "signOut": "Cerrar sesión", + "authenticated": "Sesión iniciada en Kimi Code", + "deviceCodeHelp": "Introduce este código de dispositivo en la página de autorización de Kimi:", + "docs": "Documentación de Kimi Code" + }, "zaiApiKey": "Clave API de Z AI", "getZaiApiKey": "Obtener clave API de Z AI", "zaiEntrypoint": "Punto de entrada de Z AI", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 9d5178f90f..03c64cc1e9 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Clé API Moonshot", "getMoonshotApiKey": "Obtenir la clé API Moonshot", "moonshotBaseUrl": "Point d'entrée Moonshot", + "kimiCode": { + "authMethod": "Méthode d'authentification", + "oauth": "Abonnement Kimi Code (OAuth)", + "apiKey": "Clé API Kimi Code", + "apiKeyLabel": "Clé API Kimi Code", + "signIn": "Se connecter à Kimi Code", + "signOut": "Se déconnecter", + "authenticated": "Connecté à Kimi Code", + "deviceCodeHelp": "Saisissez ce code d'appareil sur la page d'autorisation Kimi :", + "docs": "Documentation Kimi Code" + }, "zaiApiKey": "Clé API Z AI", "getZaiApiKey": "Obtenir la clé API Z AI", "zaiEntrypoint": "Point d'entrée Z AI", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2072485021..2f6c6fc542 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API कुंजी", "getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें", "moonshotBaseUrl": "Moonshot प्रवेश बिंदु", + "kimiCode": { + "authMethod": "प्रमाणीकरण विधि", + "oauth": "Kimi Code सदस्यता (OAuth)", + "apiKey": "Kimi Code API कुंजी", + "apiKeyLabel": "Kimi Code API कुंजी", + "signIn": "Kimi Code में साइन इन करें", + "signOut": "साइन आउट करें", + "authenticated": "Kimi Code में साइन इन किया गया", + "deviceCodeHelp": "Kimi प्राधिकरण पृष्ठ पर यह डिवाइस कोड दर्ज करें:", + "docs": "Kimi Code दस्तावेज़ीकरण" + }, "zaiApiKey": "Z AI API कुंजी", "getZaiApiKey": "Z AI API कुंजी प्राप्त करें", "zaiEntrypoint": "Z AI प्रवेश बिंदु", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 0b30cb84f4..f98af2bc28 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Kunci API Moonshot", "getMoonshotApiKey": "Dapatkan Kunci API Moonshot", "moonshotBaseUrl": "Titik Masuk Moonshot", + "kimiCode": { + "authMethod": "Metode autentikasi", + "oauth": "Langganan Kimi Code (OAuth)", + "apiKey": "Kunci API Kimi Code", + "apiKeyLabel": "Kunci API Kimi Code", + "signIn": "Masuk ke Kimi Code", + "signOut": "Keluar", + "authenticated": "Masuk ke Kimi Code", + "deviceCodeHelp": "Masukkan kode perangkat ini di halaman otorisasi Kimi:", + "docs": "Dokumentasi Kimi Code" + }, "zaiApiKey": "Kunci API Z AI", "getZaiApiKey": "Dapatkan Kunci API Z AI", "zaiEntrypoint": "Titik Masuk Z AI", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 125e6a725f..ea66a2ad23 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Chiave API Moonshot", "getMoonshotApiKey": "Ottieni chiave API Moonshot", "moonshotBaseUrl": "Punto di ingresso Moonshot", + "kimiCode": { + "authMethod": "Metodo di autenticazione", + "oauth": "Abbonamento Kimi Code (OAuth)", + "apiKey": "Chiave API Kimi Code", + "apiKeyLabel": "Chiave API Kimi Code", + "signIn": "Accedi a Kimi Code", + "signOut": "Esci", + "authenticated": "Accesso a Kimi Code effettuato", + "deviceCodeHelp": "Inserisci questo codice dispositivo nella pagina di autorizzazione Kimi:", + "docs": "Documentazione Kimi Code" + }, "zaiApiKey": "Chiave API Z AI", "getZaiApiKey": "Ottieni chiave API Z AI", "zaiEntrypoint": "Punto di ingresso Z AI", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index dd07efdd9a..eed8315620 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot APIキー", "getMoonshotApiKey": "Moonshot APIキーを取得", "moonshotBaseUrl": "Moonshot エントリーポイント", + "kimiCode": { + "authMethod": "認証方法", + "oauth": "Kimi Code サブスクリプション (OAuth)", + "apiKey": "Kimi Code API キー", + "apiKeyLabel": "Kimi Code API キー", + "signIn": "Kimi Code にサインイン", + "signOut": "サインアウト", + "authenticated": "Kimi Code にサインイン済み", + "deviceCodeHelp": "Kimi 認証ページでこのデバイスコードを入力してください:", + "docs": "Kimi Code ドキュメント" + }, "zaiApiKey": "Z AI APIキー", "getZaiApiKey": "Z AI APIキーを取得", "zaiEntrypoint": "Z AI エントリーポイント", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 36a4ee14e0..ae8fd3b9ef 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API 키", "getMoonshotApiKey": "Moonshot API 키 받기", "moonshotBaseUrl": "Moonshot 엔트리포인트", + "kimiCode": { + "authMethod": "인증 방법", + "oauth": "Kimi Code 구독 (OAuth)", + "apiKey": "Kimi Code API 키", + "apiKeyLabel": "Kimi Code API 키", + "signIn": "Kimi Code에 로그인", + "signOut": "로그아웃", + "authenticated": "Kimi Code에 로그인됨", + "deviceCodeHelp": "Kimi 인증 페이지에서 이 디바이스 코드를 입력하세요:", + "docs": "Kimi Code 문서" + }, "zaiApiKey": "Z AI API 키", "getZaiApiKey": "Z AI API 키 받기", "zaiEntrypoint": "Z AI 엔트리포인트", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 5c36d91e30..45b92345d9 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API-sleutel", "getMoonshotApiKey": "Moonshot API-sleutel ophalen", "moonshotBaseUrl": "Moonshot-ingangspunt", + "kimiCode": { + "authMethod": "Authenticatiemethode", + "oauth": "Kimi Code-abonnement (OAuth)", + "apiKey": "Kimi Code API-sleutel", + "apiKeyLabel": "Kimi Code API-sleutel", + "signIn": "Inloggen bij Kimi Code", + "signOut": "Uitloggen", + "authenticated": "Ingelogd bij Kimi Code", + "deviceCodeHelp": "Voer deze apparaatcode in op de Kimi-autorisatiepagina:", + "docs": "Kimi Code-documentatie" + }, "zaiApiKey": "Z AI API-sleutel", "getZaiApiKey": "Z AI API-sleutel ophalen", "zaiEntrypoint": "Z AI-ingangspunt", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 9f9da60b68..bf35f445dd 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Klucz API Moonshot", "getMoonshotApiKey": "Uzyskaj klucz API Moonshot", "moonshotBaseUrl": "Punkt wejścia Moonshot", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Klucz API Z AI", "getZaiApiKey": "Uzyskaj klucz API Z AI", "zaiEntrypoint": "Punkt wejścia Z AI", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index b267f41573..99d6013a89 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Chave de API Moonshot", "getMoonshotApiKey": "Obter chave de API Moonshot", "moonshotBaseUrl": "Ponto de entrada Moonshot", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Chave de API Z AI", "getZaiApiKey": "Obter chave de API Z AI", "zaiEntrypoint": "Ponto de entrada Z AI", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 2d427202f9..a29da12326 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API-ключ", "getMoonshotApiKey": "Получить Moonshot API-ключ", "moonshotBaseUrl": "Точка входа Moonshot", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Z AI API-ключ", "getZaiApiKey": "Получить Z AI API-ключ", "zaiEntrypoint": "Точка входа Z AI", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index ba4b092cf3..13c0b9c37c 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API Anahtarı", "getMoonshotApiKey": "Moonshot API Anahtarı Al", "moonshotBaseUrl": "Moonshot Giriş Noktası", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Z AI API Anahtarı", "getZaiApiKey": "Z AI API Anahtarı Al", "zaiEntrypoint": "Z AI Giriş Noktası", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 56a04f6c59..b59313d47a 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Khóa API Moonshot", "getMoonshotApiKey": "Lấy khóa API Moonshot", "moonshotBaseUrl": "Điểm vào Moonshot", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "zaiApiKey": "Khóa API Z AI", "getZaiApiKey": "Lấy khóa API Z AI", "zaiEntrypoint": "Điểm vào Z AI", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 03fc075da9..1b408875d4 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -459,6 +459,17 @@ "moonshotApiKey": "Moonshot API 密钥", "getMoonshotApiKey": "获取 Moonshot API 密钥", "moonshotBaseUrl": "Moonshot 服务站点", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "minimaxApiKey": "MiniMax API 密钥", "getMiniMaxApiKey": "获取 MiniMax API 密钥", "minimaxBaseUrl": "MiniMax 服务站点", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index b84cb627e7..481bab5301 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -486,6 +486,17 @@ "moonshotApiKey": "Moonshot API 金鑰", "getMoonshotApiKey": "取得 Moonshot API 金鑰", "moonshotBaseUrl": "Moonshot 服務端點", + "kimiCode": { + "authMethod": "Authentication method", + "oauth": "Kimi Code subscription (OAuth)", + "apiKey": "Kimi Code API key", + "apiKeyLabel": "Kimi Code API Key", + "signIn": "Sign in to Kimi Code", + "signOut": "Sign out", + "authenticated": "Signed in to Kimi Code", + "deviceCodeHelp": "Enter this device code in the Kimi authorization page:", + "docs": "Kimi Code documentation" + }, "minimaxApiKey": "MiniMax API 金鑰", "getMiniMaxApiKey": "取得 MiniMax API 金鑰", "minimaxBaseUrl": "MiniMax 服務端點", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 700bf27891..8aebe3f8e5 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -54,6 +54,7 @@ describe("Model Validation Functions", () => { "opencode-go": {}, kenari: {}, "zoo-gateway": {}, + "kimi-code": {}, moonshot: {}, } @@ -366,6 +367,48 @@ describe("Model Validation Functions", () => { }) }) }) + + describe("Kimi Code validation", () => { + it("returns undefined when using OAuth auth method", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "oauth", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBeUndefined() + }) + + it("returns undefined when auth method is not specified (defaults to OAuth)", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBeUndefined() + }) + + it("returns apiKey error when using api-key auth method without key", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBe("settings:validation.apiKey") + }) + + it("returns undefined when using api-key auth method with key", () => { + const config: ProviderSettings = { + apiProvider: "kimi-code", + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "valid-key", + } + + const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + expect(result).toBeUndefined() + }) + }) }) describe("validateBedrockArn", () => { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index d780786808..0a812e9d48 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -127,6 +127,11 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.qwenCodeOauthPath") } break + case "kimi-code": + if ((apiConfiguration.kimiCodeAuthMethod ?? "oauth") === "api-key" && !apiConfiguration.kimiCodeApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case "vercel-ai-gateway": if (!apiConfiguration.vercelAiGatewayApiKey) { return i18next.t("settings:validation.apiKey") From e0e7fb919b6856871a0ee767e5c432c188eb7fd7 Mon Sep 17 00:00:00 2001 From: T Date: Fri, 24 Jul 2026 12:27:50 -0400 Subject: [PATCH 52/89] [Fix] GPT-5.6 Luna requests rejected by Codex backend when using ChatGPT OAuth (#889) * fix(openai-codex): support Luna Responses Lite requests * Nuke md file that shouldnt have been commited in the first place * fix(openai-codex): address Luna Responses Lite review feedback - Capture telemetry when transformLunaResponsesLiteBody throws in createMessage - Drop weak Record alias from transformLunaResponsesLiteBody signature - Document intentional tool_choice/parallel_tool_calls and reasoning context overrides - Strengthen tests: full input array assertion, reasoning context overwrite case, retry header coverage, single-build spy, and uuidv7 session shape check --------- Co-authored-by: Naved Merchant Co-authored-by: Roomote --- .../openai-codex-native-tool-calls.spec.ts | 16 + .../providers/__tests__/openai-codex.spec.ts | 402 +++++++++++++++++- src/api/providers/openai-codex.ts | 145 +++++-- 3 files changed, 533 insertions(+), 30 deletions(-) diff --git a/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts b/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts index 80ab4e1887..2ad5dc8d61 100644 --- a/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts +++ b/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts @@ -515,6 +515,22 @@ describe("OpenAiCodexHandler native tool calls", () => { await expect(handler.completePrompt("Test prompt")).resolves.toBe("done") + const fetchOptions = mockFetch.mock.calls[0][1] + const body = JSON.parse(fetchOptions.body) + expect(body.input).toEqual([ + { + role: "user", + content: [{ type: "input_text", text: "Test prompt" }], + }, + ]) + expect(body).not.toHaveProperty("prompt_cache_key") + expect(body.reasoning?.context).toBeUndefined() + expect(body.input).not.toContainEqual(expect.objectContaining({ type: "additional_tools" })) + expect(body.input).not.toContainEqual(expect.objectContaining({ role: "developer" })) + expect(fetchOptions.headers).not.toHaveProperty("session-id") + expect(fetchOptions.headers).not.toHaveProperty("x-session-affinity") + expect(fetchOptions.headers).not.toHaveProperty("version") + expect(fetchOptions.headers).not.toHaveProperty("x-openai-internal-codex-responses-lite") expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining("/responses"), expect.objectContaining({ diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index 22466af3ce..c4249f4183 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -1,9 +1,39 @@ // npx vitest run api/providers/__tests__/openai-codex.spec.ts +vitest.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureException: vitest.fn(), + }, + }, +})) + import { Anthropic } from "@anthropic-ai/sdk" -import { OpenAiCodexHandler } from "../openai-codex" +import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" +function createCompletedStream() { + return { + async *[Symbol.asyncIterator]() { + yield { + type: "response.completed", + response: { + id: "response-1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, + } + }, + } +} + +async function drainStream(stream: AsyncIterable) { + for await (const _chunk of stream) { + // Drain the response stream. + } +} + describe("OpenAiCodexHandler.getModel", () => { it.each(["gpt-5.1", "gpt-5", "gpt-5.1-codex", "gpt-5-codex", "gpt-5-codex-mini", "gpt-5.3-codex-spark"])( "should return specified model when a valid model id is provided: %s", @@ -145,3 +175,373 @@ describe("OpenAiCodexHandler.createMessage", () => { }) }) }) + +describe("transformLunaResponsesLiteBody", () => { + it("creates the exact Responses Lite body while preserving unrelated fields and reasoning", () => { + const tools = [{ type: "function", name: "read_file", parameters: { type: "object" } }] + const input = [ + { + role: "user", + content: [ + { type: "input_text", text: "Inspect the image", detail: "keep-text-detail" }, + { + type: "input_image", + image_url: "data:image/png;base64,abc", + detail: "high", + metadata: { detail: "keep-nested-detail" }, + }, + ], + detail: "keep-message-detail", + }, + { + type: "wrapper", + detail: "keep-wrapper-detail", + items: [{ type: "input_image", image_url: "nested", detail: "low", extra: true }], + }, + ] + const body = { + model: "gpt-5.6-luna", + input, + stream: true, + store: false, + instructions: "Follow these exact instructions.", + tools, + tool_choice: { type: "function", name: "read_file" }, + parallel_tool_calls: true, + reasoning: { effort: "high", summary: "auto" }, + include: ["reasoning.encrypted_content"], + custom_field: { preserved: true }, + } + + expect(transformLunaResponsesLiteBody(body, "task-123")).toEqual({ + model: "gpt-5.6-luna", + input: [ + { type: "additional_tools", role: "developer", tools }, + { + type: "message", + role: "developer", + content: [{ type: "input_text", text: "Follow these exact instructions." }], + }, + { + role: "user", + content: [ + { type: "input_text", text: "Inspect the image", detail: "keep-text-detail" }, + { + type: "input_image", + image_url: "data:image/png;base64,abc", + metadata: { detail: "keep-nested-detail" }, + }, + ], + detail: "keep-message-detail", + }, + { + type: "wrapper", + detail: "keep-wrapper-detail", + items: [{ type: "input_image", image_url: "nested", extra: true }], + }, + ], + stream: true, + store: false, + tool_choice: "auto", + parallel_tool_calls: false, + prompt_cache_key: "task-123", + reasoning: { effort: "high", summary: "auto", context: "all_turns" }, + include: ["reasoning.encrypted_content"], + custom_field: { preserved: true }, + }) + }) + + it("uses empty additional tools, omits an empty instruction message, and creates reasoning context", () => { + const input = [{ role: "user", content: [{ type: "input_text", text: "Hello" }] }] + + expect( + transformLunaResponsesLiteBody( + { + model: "gpt-5.6-luna", + input, + instructions: "", + stream: false, + }, + "session-fallback", + ), + ).toEqual({ + model: "gpt-5.6-luna", + input: [{ type: "additional_tools", role: "developer", tools: [] }, ...input], + stream: false, + tool_choice: "auto", + parallel_tool_calls: false, + prompt_cache_key: "session-fallback", + reasoning: { context: "all_turns" }, + }) + }) + + it("overwrites a pre-existing reasoning context with all_turns", () => { + const result = transformLunaResponsesLiteBody( + { + model: "gpt-5.6-luna", + input: [{ role: "user", content: [{ type: "input_text", text: "Hello" }] }], + reasoning: { effort: "high", context: "current_turn" }, + }, + "session-1", + ) + + expect(result.reasoning).toEqual({ effort: "high", context: "all_turns" }) + }) + + it.each([ + ["input", { input: "invalid" }, "input must be an array"], + ["tools", { input: [], tools: {} }, "tools must be an array when provided"], + ["instructions", { input: [], instructions: [] }, "instructions must be a string when provided"], + ])("rejects malformed %s locally", (_field, body, expectedMessage) => { + expect(() => transformLunaResponsesLiteBody(body, "session-1")).toThrow(expectedMessage) + }) +}) + +describe("OpenAiCodexHandler Luna Responses Lite requests", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + it("uses a single task session ID in the Luna SDK body and headers", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-luna", reasoningEffort: "high" }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + ;(handler as any).client = { responses: { create: mockCreate } } + + await drainStream( + handler.createMessage("Luna instructions", [{ role: "user", content: "Hello" }], { + taskId: "task-luna", + tools: [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { type: "object", properties: { path: { type: "string" } } }, + }, + }, + ], + tool_choice: { type: "function", function: { name: "read_file" } }, + parallelToolCalls: true, + }), + ) + + const [body, options] = mockCreate.mock.calls[0] + expect(body).toMatchObject({ + model: "gpt-5.6-luna", + prompt_cache_key: "task-luna", + tool_choice: "auto", + parallel_tool_calls: false, + reasoning: { effort: "high", summary: "auto", context: "all_turns" }, + }) + expect(body).not.toHaveProperty("tools") + expect(body).not.toHaveProperty("instructions") + expect(body.input).toHaveLength(3) + expect(body.input[0]).toMatchObject({ type: "additional_tools", role: "developer" }) + expect(body.input[1]).toEqual({ + type: "message", + role: "developer", + content: [{ type: "input_text", text: "Luna instructions" }], + }) + expect(body.input[2]).toEqual({ + role: "user", + content: [{ type: "input_text", text: "Hello" }], + }) + expect(options.headers).toMatchObject({ + originator: "zoo-code", + session_id: "task-luna", + "session-id": "task-luna", + "x-session-affinity": "task-luna", + version: "0.144.0", + "x-openai-internal-codex-responses-lite": "true", + "ChatGPT-Account-Id": "acct_test", + }) + }) + + it("reuses the unchanged Luna body and headers in the manual SSE fallback", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-luna" }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + let sdkBody: any + const mockCreate = vitest.fn().mockImplementation((body: any) => { + sdkBody = body + throw new Error("SDK unavailable") + }) + ;(handler as any).client = { responses: { create: mockCreate } } + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.completed","response":{"id":"response-1","output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n', + ), + ) + controller.close() + }, + }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await drainStream( + handler.createMessage("Instructions", [{ role: "user", content: "Fallback" }], { + taskId: "task-fallback", + tools: [], + }), + ) + + const fetchOptions = mockFetch.mock.calls[0][1] + expect(JSON.parse(fetchOptions.body)).toEqual(sdkBody) + expect(fetchOptions.headers).toMatchObject({ + Authorization: "Bearer test-token", + session_id: "task-fallback", + "session-id": "task-fallback", + "x-session-affinity": "task-fallback", + version: "0.144.0", + "x-openai-internal-codex-responses-lite": "true", + }) + expect(sdkBody.prompt_cache_key).toBe("task-fallback") + }) + + it("preserves Luna session affinity while retrying with refreshed authentication", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-luna" }) + const transformSpy = vitest.spyOn(handler as any, "buildLunaRequestBody") + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("expired-token") + vitest.spyOn(openAiCodexOAuthManager, "forceRefreshAccessToken").mockResolvedValue("refreshed-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + ;(handler as any).client = { + responses: { create: vitest.fn().mockRejectedValue(new Error("SDK unavailable")) }, + } + const mockFetch = vitest + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + text: vitest.fn().mockResolvedValue('{"error":{"message":"Codex API invalid token"}}'), + }) + .mockResolvedValueOnce({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.completed","response":{"id":"response-1","output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n', + ), + ) + controller.close() + }, + }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await drainStream( + handler.createMessage("Instructions", [{ role: "user", content: "Retry" }], { + taskId: "task-retry", + tools: [], + }), + ) + + expect(openAiCodexOAuthManager.forceRefreshAccessToken).toHaveBeenCalledOnce() + expect(mockFetch).toHaveBeenCalledTimes(2) + const firstOptions = mockFetch.mock.calls[0][1] + const retryOptions = mockFetch.mock.calls[1][1] + const firstBody = JSON.parse(firstOptions.body) + const retryBody = JSON.parse(retryOptions.body) + + expect(retryBody).toEqual(firstBody) + expect(transformSpy).toHaveBeenCalledTimes(1) + expect(firstBody.prompt_cache_key).toBe("task-retry") + expect(firstOptions.headers).toMatchObject({ + Authorization: "Bearer expired-token", + "session-id": "task-retry", + "x-session-affinity": "task-retry", + version: "0.144.0", + "x-openai-internal-codex-responses-lite": "true", + }) + expect(retryOptions.headers).toMatchObject({ + Authorization: "Bearer refreshed-token", + "session-id": "task-retry", + "x-session-affinity": "task-retry", + version: "0.144.0", + "x-openai-internal-codex-responses-lite": "true", + }) + }) + + it.each(["gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna-alias"])( + "does not apply Luna behavior to %s", + async (apiModelId) => { + const handler = new OpenAiCodexHandler({ apiModelId }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + ;(handler as any).client = { responses: { create: mockCreate } } + + await drainStream( + handler.createMessage("Normal instructions", [{ role: "user", content: "Hello" }], { + taskId: "task-normal", + tools: [], + tool_choice: "required", + parallelToolCalls: true, + }), + ) + + const [body, options] = mockCreate.mock.calls[0] + expect(body.instructions).toBe("Normal instructions") + expect(body.tools).toEqual([]) + expect(body.tool_choice).toBe("required") + expect(body.parallel_tool_calls).toBe(true) + expect(body).not.toHaveProperty("prompt_cache_key") + expect(body.reasoning?.context).toBeUndefined() + expect(options.headers).not.toHaveProperty("session-id") + expect(options.headers).not.toHaveProperty("x-session-affinity") + expect(options.headers).not.toHaveProperty("version") + expect(options.headers).not.toHaveProperty("x-openai-internal-codex-responses-lite") + }, + ) + + it("uses the handler session for reasoning-disabled Luna completePrompt requests", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-luna", reasoningEffort: "disable" }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + json: vitest.fn().mockResolvedValue({ + output: [ + { + type: "message", + content: [{ type: "output_text", text: "Complete" }], + }, + ], + }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await expect(handler.completePrompt("Hello Luna")).resolves.toBe("Complete") + + const fetchOptions = mockFetch.mock.calls[0][1] + const body = JSON.parse(fetchOptions.body) + const sessionId = body.prompt_cache_key + expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + expect(body).toMatchObject({ + model: "gpt-5.6-luna", + stream: false, + tool_choice: "auto", + parallel_tool_calls: false, + reasoning: { context: "all_turns" }, + }) + expect(body).not.toHaveProperty("include") + expect(body.input).toEqual([ + { type: "additional_tools", role: "developer", tools: [] }, + { role: "user", content: [{ type: "input_text", text: "Hello Luna" }] }, + ]) + expect(fetchOptions.headers).toMatchObject({ + session_id: sessionId, + "session-id": sessionId, + "x-session-affinity": sessionId, + version: "0.144.0", + "x-openai-internal-codex-responses-lite": "true", + }) + }) +}) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 374a715b34..27ddbe19e2 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -34,6 +34,68 @@ export type OpenAiCodexModel = ReturnType * Per the implementation guide: requests are routed to chatgpt.com/backend-api/codex */ const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex" +const LUNA_MODEL_ID = "gpt-5.6-luna" +const LUNA_CODEX_VERSION = "0.144.0" + +function stripInputImageDetail(value: any): any { + if (Array.isArray(value)) { + return value.map(stripInputImageDetail) + } + + if (!value || typeof value !== "object") { + return value + } + + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => value.type !== "input_image" || key !== "detail") + .map(([key, child]) => [key, stripInputImageDetail(child)]), + ) +} + +export function transformLunaResponsesLiteBody(requestBody: any, effectiveSessionId: string): any { + if (!Array.isArray(requestBody.input)) { + throw new Error("Invalid gpt-5.6-luna Responses Lite request: input must be an array.") + } + if (requestBody.tools !== undefined && !Array.isArray(requestBody.tools)) { + throw new Error("Invalid gpt-5.6-luna Responses Lite request: tools must be an array when provided.") + } + if (requestBody.instructions !== undefined && typeof requestBody.instructions !== "string") { + throw new Error("Invalid gpt-5.6-luna Responses Lite request: instructions must be a string when provided.") + } + + const { tools, instructions, ...rest } = requestBody + const transformedInput = stripInputImageDetail(requestBody.input) + const reasoning = + requestBody.reasoning && typeof requestBody.reasoning === "object" && !Array.isArray(requestBody.reasoning) + ? requestBody.reasoning + : {} + + return { + ...rest, + input: [ + { type: "additional_tools", role: "developer", tools: tools ?? [] }, + ...(typeof instructions === "string" && instructions.length > 0 + ? [ + { + type: "message", + role: "developer", + content: [{ type: "input_text", text: instructions }], + }, + ] + : []), + ...transformedInput, + ], + // Luna Responses Lite requires these exact values, so they intentionally + // override any caller-supplied tool_choice or parallel_tool_calls. + tool_choice: "auto", + parallel_tool_calls: false, + prompt_cache_key: effectiveSessionId, + // Luna Responses Lite requires reasoning context "all_turns"; this intentionally + // overwrites any context value already present in the incoming reasoning config. + reasoning: { ...reasoning, context: "all_turns" }, + } +} /** * OpenAiCodexHandler - Uses OpenAI Responses API with OAuth authentication @@ -183,12 +245,26 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Build request body // Per the implementation guide: Codex backend may reject some parameters // Notably: max_output_tokens and prompt_cache_retention may be rejected - const requestBody = this.buildRequestBody(model, formattedInput, systemPrompt, reasoningEffort, metadata) + const effectiveSessionId = metadata?.taskId || this.sessionId + const baseRequestBody = this.buildRequestBody(model, formattedInput, systemPrompt, reasoningEffort, metadata) + let requestBody: any + try { + requestBody = + model.id === LUNA_MODEL_ID + ? this.buildLunaRequestBody(baseRequestBody, effectiveSessionId) + : baseRequestBody + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException( + new ApiProviderError(message, this.providerName, model.id, "createMessage"), + ) + throw error + } // Make the request with retry on auth failure for (let attempt = 0; attempt < 2; attempt++) { try { - yield* this.executeRequest(requestBody, model, accessToken, metadata?.taskId) + yield* this.executeRequest(requestBody, model, accessToken, effectiveSessionId) return } catch (error) { const message = error instanceof Error ? error.message : String(error) @@ -213,6 +289,10 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } } + private buildLunaRequestBody(baseRequestBody: any, effectiveSessionId: string): any { + return transformLunaResponsesLiteBody(baseRequestBody, effectiveSessionId) + } + private buildRequestBody( model: OpenAiCodexModel, formattedInput: any, @@ -344,7 +424,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion requestBody: any, model: OpenAiCodexModel, accessToken: string, - taskId?: string, + effectiveSessionId: string, ): ApiStream { // Create AbortController for cancellation this.abortController = new AbortController() @@ -357,12 +437,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion const accountId = await openAiCodexOAuthManager.getAccountId() // Build Codex-specific headers. Authorization is provided by the SDK apiKey. - const codexHeaders: Record = { - originator: "zoo-code", - session_id: taskId || this.sessionId, - "User-Agent": `zoo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, - ...(accountId ? { "ChatGPT-Account-Id": accountId } : {}), - } + const codexHeaders = this.buildCodexHeaders(model, effectiveSessionId, accountId) // Allow tests to inject a client. If none is injected, create one for this request. const client = @@ -400,7 +475,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } } catch (_sdkErr) { // Fallback to manual SSE via fetch (Codex backend). - yield* this.makeCodexRequest(requestBody, model, accessToken, taskId) + yield* this.makeCodexRequest(requestBody, model, accessToken, effectiveSessionId) } } finally { this.abortController = undefined @@ -494,7 +569,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion requestBody: any, model: OpenAiCodexModel, accessToken: string, - taskId?: string, + effectiveSessionId: string, ): ApiStream { // Per the implementation guide: route to Codex backend with Bearer token const url = `${CODEX_API_BASE_URL}/responses` @@ -504,16 +579,9 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Build headers with required Codex-specific fields const headers: Record = { + ...this.buildCodexHeaders(model, effectiveSessionId, accountId), "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, - originator: "zoo-code", - session_id: taskId || this.sessionId, - "User-Agent": `zoo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, - } - - // Add ChatGPT-Account-Id if available (required for organization subscriptions) - if (accountId) { - headers["ChatGPT-Account-Id"] = accountId } try { @@ -1117,6 +1185,27 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion return selected && selected !== "disable" && selected !== "none" ? (selected as any) : undefined } + private buildCodexHeaders( + model: OpenAiCodexModel, + effectiveSessionId: string, + accountId?: string | null, + ): Record { + return { + originator: "zoo-code", + session_id: effectiveSessionId, + "User-Agent": `zoo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, + ...(accountId ? { "ChatGPT-Account-Id": accountId } : {}), + ...(model.id === LUNA_MODEL_ID + ? { + "session-id": effectiveSessionId, + "x-session-affinity": effectiveSessionId, + version: LUNA_CODEX_VERSION, + "x-openai-internal-codex-responses-lite": "true", + } + : {}), + } + } + override getModel() { const modelId = this.options.apiModelId @@ -1173,7 +1262,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion const reasoningEffort = this.getReasoningEffort(model) - const requestBody: any = { + const baseRequestBody: any = { model: model.id, input: [ { @@ -1187,12 +1276,17 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } if (reasoningEffort) { - requestBody.reasoning = { + baseRequestBody.reasoning = { effort: reasoningEffort, summary: "auto" as const, } } + const requestBody = + model.id === LUNA_MODEL_ID + ? this.buildLunaRequestBody(baseRequestBody, this.sessionId) + : baseRequestBody + const url = `${CODEX_API_BASE_URL}/responses` // Get ChatGPT account ID for organization subscriptions @@ -1200,16 +1294,9 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Build headers with required Codex-specific fields const headers: Record = { + ...this.buildCodexHeaders(model, this.sessionId, accountId), "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, - originator: "zoo-code", - session_id: this.sessionId, - "User-Agent": `zoo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, - } - - // Add ChatGPT-Account-Id if available - if (accountId) { - headers["ChatGPT-Account-Id"] = accountId } const response = await fetch(url, { From 2634d5c84d57900271085cdaaf8a06ff266ef968 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:36:25 +0300 Subject: [PATCH 53/89] Add Dart support to codebase indexing (#941) * feat(tree-sitter): add Dart definition support * fix(tree-sitter): address Dart query review feedback * fix(tree-sitter): include adjacent Dart function bodies --------- Co-authored-by: Naved Merchant --- .../__tests__/fixtures/sample-dart.ts | 113 ++++++++++++++++ .../__tests__/languageParser.spec.ts | 6 + .../parseSourceCodeDefinitions.dart.spec.ts | 123 ++++++++++++++++++ src/services/tree-sitter/index.ts | 15 ++- src/services/tree-sitter/languageParser.ts | 5 + src/services/tree-sitter/queries/dart.ts | 53 ++++++++ src/services/tree-sitter/queries/index.ts | 1 + 7 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 src/services/tree-sitter/__tests__/fixtures/sample-dart.ts create mode 100644 src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts create mode 100644 src/services/tree-sitter/queries/dart.ts diff --git a/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts b/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts new file mode 100644 index 0000000000..d09f12ac35 --- /dev/null +++ b/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts @@ -0,0 +1,113 @@ +export default `abstract class Animal { + String speak(); + + Future describe({ + required bool verbose, + }); +} + +class Point { + const Point(); + Point.named(); + factory Point.origin() => const Point(); + + Point.fromCoordinates( + int x, + int y, + ) : assert(x >= 0), + assert(y >= 0); + + factory Point.fromRecord( + ({int x, int y}) coordinates, + ) => Point.fromCoordinates( + coordinates.x, + coordinates.y, + ); + + int get x => 0; + set x(int value) {} + + Point operator +(Point other) => this; + + Point operator [](int index) => this; + + static List emptyList() { + return []; + } +} + +mixin Runner { + void run() { + print("running"); + } +} + +enum Status { + ready, + running; + + const Status(); +} + +class Dog extends Animal with Runner { + final String name; + + Dog(this.name); + + @override + String speak() { + return "\$name barks"; + } +} + +extension StringTools on String { + String doubled() { + return this + this; + } +} + +extension on int { + int squared() => this * this; +} + +extension type UserId(int value) { + UserId.zero() : value = 0; +} + +typedef Operation = int Function(int left, int right); + +typedef AsyncOperation = Future Function( + T value, { + required Duration timeout, +}); + +int get answer => 42; +set answer(int value) {} + +int add(int left, int right) { + return left + right; +} + +Future retry( + Future Function() operation, { + int attempts = 3, +}) async { + return operation(); +} + +Future initialize() async { + await Future.value(); +} + +Iterable countUpTo(int maximum) sync* { + for (var value = 0; value <= maximum; value++) { + yield value; + } +} + +Stream countPeriodically(int maximum) async* { + for (var value = 0; value <= maximum; value++) { + yield value; + } +} +` diff --git a/src/services/tree-sitter/__tests__/languageParser.spec.ts b/src/services/tree-sitter/__tests__/languageParser.spec.ts index 9bf9ecc881..fe4dcdb2d9 100644 --- a/src/services/tree-sitter/__tests__/languageParser.spec.ts +++ b/src/services/tree-sitter/__tests__/languageParser.spec.ts @@ -49,6 +49,12 @@ describe("loadRequiredLanguageParsers", () => { expect(parsers.kts.query).toBeDefined() }) + it("should load Dart parser for .dart files", async () => { + const parsers = await loadRequiredLanguageParsers(["test.dart"], WASM_DIR) + expect(parsers.dart).toBeDefined() + expect(parsers.dart.query).toBeDefined() + }) + it("should throw error for unsupported file extensions", async () => { const files = ["test.unsupported"] await expect(loadRequiredLanguageParsers(files, WASM_DIR)).rejects.toThrow("Unsupported language: unsupported") diff --git a/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts new file mode 100644 index 0000000000..d7d681a263 --- /dev/null +++ b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts @@ -0,0 +1,123 @@ +import { dartQuery } from "../queries" +import { testParseSourceCodeDefinitions } from "./helpers" +import sampleDartContent from "./fixtures/sample-dart" + +const dartOptions = { + language: "dart", + wasmFile: "tree-sitter-dart.wasm", + queryString: dartQuery, + extKey: "dart", +} + +describe("parseSourceCodeDefinitionsForFile with Dart", () => { + it("captures a redirecting factory constructor", async () => { + const content = `class Logger { + factory Logger() = ConsoleLogger; + } + + class ConsoleLogger implements Logger {}` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + + expect(result).toMatch(/\d+--\d+ \|\s*factory Logger\(\) = ConsoleLogger/) + }) + + it("captures a multiline generative constructor once", async () => { + const content = `class Point { + Point.fromCoordinates( + int x, + int y, + ); + }` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + + expect(result?.match(/\d+--\d+ \|\s*Point\.fromCoordinates\(/g)).toHaveLength(1) + }) + + it("captures a mixin application class", async () => { + const content = `class Base {} + mixin Serializable {} + class SerializableBase = Base with Serializable;` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + + expect(result).toMatch(/\d+--\d+ \|\s*class SerializableBase = Base with Serializable/) + }) + + it("captures external top-level functions and methods", async () => { + const content = `external int nativeVersion(); + + class NativeApi { + external String platformName(); + }` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + + expect(result).toMatch(/\d+--\d+ \| external int nativeVersion\(\)/) + expect(result).toMatch(/\d+--\d+ \|\s*external String platformName\(\)/) + }) + + it("does not report local functions as file-level definitions", async () => { + const content = `void outer() { + int inner() => 1; + print(inner()); + }` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + + expect(result).toMatch(/\d+--\d+ \| void outer\(\)/) + expect(result).not.toMatch(/int inner\(\)/) + }) + + it("includes a multiline top-level function body without duplicating its declaration", async () => { + const content = `int add(int left, int right) { + final result = left + right; + return result; + }` + + const result = await testParseSourceCodeDefinitions("/test/file.dart", content, dartOptions) + const addDefinitions = result?.split("\n").filter((line) => line.includes("int add(")) ?? [] + + expect(addDefinitions).toEqual(["1--4 | int add(int left, int right) {"]) + }) + + it("should capture common Dart declarations", async () => { + const result = await testParseSourceCodeDefinitions("/test/file.dart", sampleDartContent, dartOptions) + const definitionLines = result?.split("\n").filter((line) => line.includes(" | ")) ?? [] + + expect(result).toMatch(/\d+--\d+ \| abstract class Animal/) + expect(result).toMatch(/\d+--\d+ \|\s*Future describe/) + expect(result).toMatch(/\d+--\d+ \| class Point/) + expect(result).toMatch(/\d+--\d+ \|\s*const Point\(\)/) + expect(result).toMatch(/\d+--\d+ \|\s*Point\.named\(\)/) + expect(result).toMatch(/\d+--\d+ \|\s*factory Point\.origin\(\)/) + expect(result).toMatch(/\d+--\d+ \|\s*Point\.fromCoordinates\(/) + expect(result).toMatch(/\d+--\d+ \|\s*factory Point\.fromRecord\(/) + expect(result?.match(/\d+--\d+ \| Point\.fromCoordinates\(/g)).toHaveLength(1) + expect(result?.match(/\d+--\d+ \| factory Point\.fromRecord\(/g)).toHaveLength(1) + expect(result).toMatch(/\d+--\d+ \|\s*int get x/) + expect(result).toMatch(/\d+--\d+ \|\s*set x\(int value\)/) + expect(result).toMatch(/\d+--\d+ \|\s*Point operator \+/) + expect(result).toMatch(/\d+--\d+ \|\s*Point operator \[]/) + expect(result).toMatch(/\d+--\d+ \|\s*static List emptyList/) + expect(result).toMatch(/\d+--\d+ \| mixin Runner/) + expect(result).toMatch(/\d+--\d+ \| enum Status/) + expect(result).toMatch(/\d+--\d+ \|\s*const Status\(\)/) + expect(result).toMatch(/\d+--\d+ \| class Dog extends Animal with Runner/) + expect(result).toMatch(/\d+--\d+ \| extension StringTools on String/) + expect(result).toMatch(/\d+--\d+ \| extension on int/) + expect(result).toMatch(/\d+--\d+ \| extension type UserId/) + expect(result).toMatch(/\d+--\d+ \| typedef Operation/) + expect(result).toMatch(/\d+--\d+ \| typedef AsyncOperation/) + expect(result).toMatch(/\d+--\d+ \| int get answer/) + expect(result).toMatch(/\d+--\d+ \| set answer\(int value\)/) + expect(result).toMatch(/\d+--\d+ \|\s*String speak\(\)/) + expect(result).toMatch(/\d+--\d+ \| int add\(int left, int right\)/) + expect(result).toMatch(/\d+--\d+ \| Future retry/) + expect(result).toMatch(/\d+--\d+ \| Future initialize\(\) async/) + expect(result).toMatch(/\d+--\d+ \| Iterable countUpTo\(int maximum\) sync\*/) + expect(result).toMatch(/\d+--\d+ \| Stream countPeriodically\(int maximum\) async\*/) + expect(definitionLines).toHaveLength(37) + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index da20a07de7..241094699d 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -93,6 +93,8 @@ const extensions = [ "erb", // Visual Basic .NET "vb", + // Dart + "dart", ].map((e) => `.${e}`) export { extensions } @@ -228,9 +230,14 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st const definitionNode = name.includes("name") ? node.parent : node if (!definitionNode) return + // Some grammars represent a definition's body as the captured signature's + // next sibling. Include that adjacent body in the definition range. + const trailingDefinitionBody = + definitionNode.nextSibling?.type === "function_body" ? definitionNode.nextSibling : undefined + // Get the start and end lines of the full definition const startLine = definitionNode.startPosition.row - const endLine = definitionNode.endPosition.row + const endLine = trailingDefinitionBody?.endPosition.row ?? definitionNode.endPosition.row const lineCount = endLine - startLine + 1 // Skip components that don't span enough lines @@ -270,9 +277,11 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st if (node.parent && node.parent.lastChild) { const contextEnd = node.parent.lastChild.endPosition.row const contextSpan = contextEnd - node.parent.startPosition.row + 1 + const hasDistinctContextStart = node.parent.startPosition.row !== startLine - // Only include context if it spans multiple lines - if (contextSpan >= getMinComponentLines()) { + // Only include context when it adds a distinct source line. A parent + // starting on the definition line would echo the same declaration. + if (hasDistinctContextStart && contextSpan >= getMinComponentLines()) { // Add the full range first const rangeKey = `${node.parent.startPosition.row}-${contextEnd}` if (!processedLines.has(rangeKey)) { diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts index a8ac0a9ead..b7faf2cdba 100644 --- a/src/services/tree-sitter/languageParser.ts +++ b/src/services/tree-sitter/languageParser.ts @@ -28,6 +28,7 @@ import { embeddedTemplateQuery, elispQuery, elixirQuery, + dartQuery, } from "./queries" export interface LanguageParser { @@ -218,6 +219,10 @@ export async function loadRequiredLanguageParsers(filesToParse: string[], source language = await loadLanguage("elixir", sourceDirectory) query = new Query(language, elixirQuery) break + case "dart": + language = await loadLanguage("dart", sourceDirectory) + query = new Query(language, dartQuery) + break default: throw new Error(`Unsupported language: ${ext}`) } diff --git a/src/services/tree-sitter/queries/dart.ts b/src/services/tree-sitter/queries/dart.ts new file mode 100644 index 0000000000..d4c67a05ac --- /dev/null +++ b/src/services/tree-sitter/queries/dart.ts @@ -0,0 +1,53 @@ +// Definition captures adapted from tree-sitter-dart's canonical tags query: +// https://github.com/UserNobody14/tree-sitter-dart/blob/master/queries/tags.scm +export default ` +(class_definition + name: (identifier) @name) @definition.class + +(class_definition + (mixin_application_class + (identifier) @name)) @definition.class + +(type_alias + (type_identifier) @name) @definition.type + +(declaration + (function_signature + name: (identifier) @name)) @definition.method + +(redirecting_factory_constructor_signature + (identifier) @name) @definition.method + +(method_signature) @definition.method + +(constructor_signature + name: (identifier) @name) @definition.method + +(constant_constructor_signature + (identifier) @name) @definition.method + +(mixin_declaration + (mixin) + (identifier) @name) @definition.mixin + +(extension_declaration + name: (identifier) @name) @definition.extension + +(extension_type_declaration + name: (identifier) @name) @definition.extension + +(enum_declaration + name: (identifier) @name) @definition.enum + +(program + (getter_signature + name: (identifier) @name) @definition.function) + +(program + (setter_signature + name: (identifier) @name) @definition.function) + +(program + (function_signature + name: (identifier) @name) @definition.function) +` diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts index de9b9cafa3..0faa256269 100644 --- a/src/services/tree-sitter/queries/index.ts +++ b/src/services/tree-sitter/queries/index.ts @@ -26,3 +26,4 @@ export { zigQuery } from "./zig" export { default as embeddedTemplateQuery } from "./embedded_template" export { elispQuery } from "./elisp" export { scalaQuery } from "./scala" +export { default as dartQuery } from "./dart" From d5a8c4a3cb2bf5578411c4b3bf2027857a3bbfec Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:55:47 -0400 Subject: [PATCH 54/89] feat: implement Claude Opus 5 support across all providers (#1010) Add full Claude Opus 5 support across all provider paths with correct model definitions, adaptive-thinking handling, fetcher overrides, and accurate pricing from Anthropic's published docs. Model definitions (anthropic.ts, bedrock.ts, vertex.ts): - 1M native context window (no beta header required) - Adaptive-thinking binary toggle (supportsReasoningBudget/Binary) - supportsTemperature: false (sampling params return a 400) - Pricing: input $5.0, output $25.0, cacheWrites $6.25, cacheReads $0.50 per million tokens Provider handling: - Add opus-5 to the Bedrock adaptive-thinking guard - Add claude-opus-5 to OpenRouter and Vercel AI Gateway allow-lists - Apply Opus 5 overrides (maxTokens, reasoning budget, temperature) in the openrouter, requesty, and vercel-ai-gateway fetchers Tests: - Add Opus 5 coverage across provider and fetcher test suites Co-authored-by: Naved Merchant <14171946+navedmerchant@users.noreply.github.com> --- packages/types/src/providers/anthropic.ts | 18 ++++++ packages/types/src/providers/bedrock.ts | 19 ++++++ packages/types/src/providers/openrouter.ts | 2 + .../types/src/providers/vercel-ai-gateway.ts | 2 + packages/types/src/providers/vertex.ts | 14 ++++ .../__tests__/anthropic-vertex.spec.ts | 46 +++++++++++++ src/api/providers/__tests__/anthropic.spec.ts | 44 +++++++++++++ src/api/providers/__tests__/bedrock.spec.ts | 64 +++++++++++++++++++ src/api/providers/__tests__/requesty.spec.ts | 64 +++++++++++++++++++ .../__tests__/vercel-ai-gateway.spec.ts | 30 +++++++++ src/api/providers/anthropic.ts | 2 + src/api/providers/bedrock.ts | 9 +-- .../fetchers/__tests__/openrouter.spec.ts | 28 ++++++++ .../fetchers/__tests__/requesty.spec.ts | 25 ++++++++ .../__tests__/vercel-ai-gateway.spec.ts | 16 +++++ src/api/providers/fetchers/openrouter.ts | 7 ++ src/api/providers/fetchers/requesty.ts | 6 ++ .../providers/fetchers/vercel-ai-gateway.ts | 4 ++ src/shared/__tests__/api.spec.ts | 27 ++++++++ 19 files changed, 423 insertions(+), 4 deletions(-) diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index f56bfb4513..f5f7e71116 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -145,6 +145,24 @@ export const anthropicModels = { supportsReasoningBinary: true, supportsTemperature: false, }, + "claude-opus-5": { + maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false. + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + inputPrice: 5.0, // $5 per million input tokens + outputPrice: 25.0, // $25 per million output tokens + cacheWritesPrice: 6.25, // $6.25 per million tokens + cacheReadsPrice: 0.5, // $0.50 per million tokens + // Opus 5 uses the same adaptive-thinking / binary-toggle convention as + // Opus 4.7+ and Sonnet 5 on the direct Anthropic provider path. Manual + // extended thinking (budget_tokens) is removed and returns a 400, and + // setting sampling parameters (temperature/top_p/top_k) returns a 400. + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", + }, "claude-fable-5": { maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false. contextWindow: 1_000_000, diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index 70fbe16057..78fea48845 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -235,6 +235,23 @@ export const bedrockModels = { }, ], }, + "anthropic.claude-opus-5": { + maxTokens: 8192, + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + inputPrice: 5.0, // $5 per million input tokens + outputPrice: 25.0, // $25 per million output tokens + cacheWritesPrice: 6.25, // $6.25 per million tokens + cacheReadsPrice: 0.5, // $0.50 per million tokens + minTokensPerCachePoint: 1024, + maxCachePoints: 4, + cachableFields: ["system", "messages", "tools"], + description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", + }, "anthropic.claude-fable-5": { maxTokens: 8192, contextWindow: 1_000_000, @@ -625,6 +642,7 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [ // - Claude Opus 4.5 // - Claude Opus 4.6 // - Claude Opus 4.7 +// - Claude Opus 5 // - Claude Fable 5 (cross-region inference only — can only be used through an inference profile) export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-sonnet-4-20250514-v1:0", @@ -636,6 +654,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-opus-4-6-v1", "anthropic.claude-opus-4-7", "anthropic.claude-opus-4-8", + "anthropic.claude-opus-5", "anthropic.claude-fable-5", ] as const diff --git a/packages/types/src/providers/openrouter.ts b/packages/types/src/providers/openrouter.ts index 9b904bd6a9..9f146a304b 100644 --- a/packages/types/src/providers/openrouter.ts +++ b/packages/types/src/providers/openrouter.ts @@ -44,6 +44,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", + "anthropic/claude-opus-5", "anthropic/claude-fable-5", "anthropic/claude-haiku-4.5", "google/gemini-2.5-flash-preview", @@ -76,6 +77,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([ "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", + "anthropic/claude-opus-5", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index a231dd51d3..f44df88656 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -13,6 +13,7 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", + "anthropic/claude-opus-5", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.6", @@ -58,6 +59,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", + "anthropic/claude-opus-5", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index d6a156e3fe..46ff835682 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -462,6 +462,20 @@ export const vertexModels = { }, ], }, + "claude-opus-5": { + maxTokens: 8192, + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + inputPrice: 5.0, // $5 per million input tokens + outputPrice: 25.0, // $25 per million output tokens + cacheWritesPrice: 6.25, // $6.25 per million tokens + cacheReadsPrice: 0.5, // $0.50 per million tokens + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", + }, "claude-fable-5": { maxTokens: 8192, contextWindow: 1_000_000, diff --git a/src/api/providers/__tests__/anthropic-vertex.spec.ts b/src/api/providers/__tests__/anthropic-vertex.spec.ts index d697757853..706bce58e7 100644 --- a/src/api/providers/__tests__/anthropic-vertex.spec.ts +++ b/src/api/providers/__tests__/anthropic-vertex.spec.ts @@ -1085,6 +1085,23 @@ describe("VertexHandler", () => { expect(model.info.supportsTemperature).toBe(false) }) + it("should return Claude Opus 5 model info", () => { + const handler = new AnthropicVertexHandler({ + apiModelId: "claude-opus-5", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + + const model = handler.getModel() + expect(model.id).toBe("claude-opus-5") + expect(model.info.maxTokens).toBe(8192) + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + }) + it("should not enable 1M context when flag is disabled", () => { const handler = new AnthropicVertexHandler({ apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0], @@ -1392,6 +1409,35 @@ describe("VertexHandler", () => { expect(request.thinking).not.toHaveProperty("budget_tokens") expect(request.temperature).toBeUndefined() }) + + it("should use adaptive thinking for Claude Opus 5", async () => { + const opusHandler = new AnthropicVertexHandler({ + apiModelId: "claude-opus-5", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + enableReasoningEffort: true, + }) + + const mockCreate = vitest.fn().mockImplementation(async () => ({ + async *[Symbol.asyncIterator]() { + yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } } + }, + })) + ;(opusHandler["client"].messages as any).create = mockCreate + + await opusHandler.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }]).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking: { type: "adaptive" }, + }), + undefined, + ) + + const request = mockCreate.mock.calls[0][0] + expect(request.thinking).not.toHaveProperty("budget_tokens") + expect(request.temperature).toBeUndefined() + }) }) describe("native tool calling", () => { diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index c17e486184..686d1949d8 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -456,6 +456,33 @@ describe("AnthropicHandler", () => { expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") }) + it("should use adaptive thinking for Claude Opus 5 when reasoning is enabled", async () => { + const opusHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-opus-5", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }) + + const stream = opusHandler.createMessage(systemPrompt, [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + ]) + + for await (const _chunk of stream) { + // Consume stream + } + + const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + expect(requestBody?.thinking).toEqual({ type: "adaptive" }) + expect(requestBody?.temperature).toBeUndefined() + expect(requestBody?.max_tokens).toBe(32768) + expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") + }) + it("should send the custom model ID as-is and use adaptive thinking for a custom Sonnet-5-family model", async () => { const customHandler = new AnthropicHandler({ apiKey: "test-api-key", @@ -656,6 +683,23 @@ describe("AnthropicHandler", () => { expect(model.reasoningBudget).toBeUndefined() }) + it("should handle Claude Opus 5 model correctly", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-opus-5", + }) + const model = handler.getModel() + expect(model.id).toBe("claude-opus-5") + expect(model.info.maxTokens).toBe(128000) + expect(model.info.contextWindow).toBe(1000000) + expect(model.maxTokens).toBe(8192) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.reasoningBudget).toBeUndefined() + }) + it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => { const handler = new AnthropicHandler({ apiKey: "test-api-key", diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index e2b17e4e87..35856e8705 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -755,6 +755,37 @@ describe("AwsBedrockHandler", () => { const model = handler.getModel() expect(model.id).toBe("global.anthropic.claude-sonnet-5") }) + + it("should return Claude Opus 5 model info", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-opus-5", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-opus-5") + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.maxTokens).toBe(8192) + }) + + it("should apply global inference prefix for Claude Opus 5 when awsUseGlobalInference is true", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-opus-5", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseGlobalInference: true, + }) + + const model = handler.getModel() + expect(model.id).toBe("global.anthropic.claude-opus-5") + }) }) describe("1M context beta feature", () => { @@ -1487,6 +1518,36 @@ describe("AwsBedrockHandler", () => { expect(commandArg.inferenceConfig?.temperature).toBeUndefined() }) + it("should send adaptive thinking with effort xhigh for Claude Opus 5 when reasoning is enabled", async () => { + // End-to-end regression guard for the Opus 5 handler branch. The + // isAdaptiveThinkingModel predicate is unit-covered, but a regression in + // the createMessage adaptive-thinking branch for this specific model + // wouldn't be caught without a request-level test. + const opus5Handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-opus-5", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + enableReasoningEffort: true, + }) + + const generator = opus5Handler.createMessage("System prompt", messages) + await generator.next() + + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any + + // Opus 5 uses the same adaptive-thinking contract as Opus 4.7/4.8 — + // budget_tokens causes a 400, so thinking.type is "adaptive" with effort. + expect(commandArg.additionalModelRequestFields?.thinking).toEqual({ + type: "adaptive", + display: "summarized", + }) + expect(commandArg.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" }) + // Opus 5 rejects sampling parameters: temperature must be omitted entirely. + expect(commandArg.inferenceConfig?.temperature).toBeUndefined() + }) + it("should omit thinking and temperature for Claude Opus 4.8 when reasoning is disabled", async () => { const opus48Handler = new AwsBedrockHandler({ apiModelId: "anthropic.claude-opus-4-8", @@ -1620,6 +1681,7 @@ describe("AwsBedrockHandler", () => { expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-8")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-fable-5")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-5")).toBe(true) + expect(isAdaptiveThinkingModel("anthropic.claude-opus-5")).toBe(true) // Future-proof Sonnet patterns — guarded even before a registry entry exists. expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-7")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-8")).toBe(true) @@ -1629,12 +1691,14 @@ describe("AwsBedrockHandler", () => { expect(isAdaptiveThinkingModel("us.anthropic.claude-opus-4-8")).toBe(true) expect(isAdaptiveThinkingModel("global.anthropic.claude-fable-5")).toBe(true) expect(isAdaptiveThinkingModel("global.anthropic.claude-sonnet-5")).toBe(true) + expect(isAdaptiveThinkingModel("global.anthropic.claude-opus-5")).toBe(true) expect(isAdaptiveThinkingModel("eu.anthropic.claude-sonnet-4-7")).toBe(true) expect(isAdaptiveThinkingModel("global.anthropic.claude-opus-4-8")).toBe(true) }) it("returns false for older / non-adaptive models", () => { expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-6-v1")).toBe(false) + expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-5-20251101-v1:0")).toBe(false) expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-6")).toBe(false) expect(isAdaptiveThinkingModel("anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe(false) expect(isAdaptiveThinkingModel("amazon.nova-lite-v1:0")).toBe(false) diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index 5d829f2374..38e0c33d95 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -78,6 +78,20 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 0.3, description: "Claude Sonnet 5", }, + "anthropic/claude-opus-5": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + inputPrice: 5, + outputPrice: 25, + cacheWritesPrice: 6.25, + cacheReadsPrice: 0.5, + description: "Claude Opus 5", + }, }) }), })) @@ -301,6 +315,39 @@ describe("RequestyHandler", () => { ) }) + it("uses adaptive thinking for Claude Opus 5 when reasoning is enabled", async () => { + const handler = new RequestyHandler({ + requestyApiKey: "test-key", + requestyModelId: "anthropic/claude-opus-5", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + } + }, + } + + mockCreate.mockResolvedValue(mockStream) + + const generator = handler.createMessage("test system prompt", [{ role: "user" as const, content: "test" }]) + await generator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "anthropic/claude-opus-5", + max_tokens: 32768, + thinking: { type: "adaptive" }, + temperature: undefined, + }), + ) + }) + it("handles API errors", async () => { const handler = new RequestyHandler(mockOptions) const mockError = new Error("API Error") @@ -587,6 +634,23 @@ describe("RequestyHandler", () => { }) }) + it("omits temperature for Claude Opus 5 in completePrompt", async () => { + const handler = new RequestyHandler({ + requestyApiKey: "test-key", + requestyModelId: "anthropic/claude-opus-5", + }) + mockCreate.mockResolvedValue({ choices: [{ message: { content: "test completion" } }] }) + + await handler.completePrompt("test prompt") + + expect(mockCreate).toHaveBeenCalledWith({ + model: "anthropic/claude-opus-5", + max_tokens: 8192, + messages: [{ role: "system", content: "test prompt" }], + temperature: undefined, + }) + }) + it("handles API errors", async () => { const handler = new RequestyHandler(mockOptions) const mockError = new Error("API Error") diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index 4669904e2f..ad14486262 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -61,6 +61,18 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 0.3, description: "Claude Sonnet 5", }, + "anthropic/claude-opus-5": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + supportsTemperature: false, + inputPrice: 5, + outputPrice: 25, + cacheWritesPrice: 6.25, + cacheReadsPrice: 0.5, + description: "Claude Opus 5", + }, "anthropic/claude-3.5-haiku": { maxTokens: 32000, contextWindow: 200000, @@ -335,6 +347,24 @@ describe("VercelAiGatewayHandler", () => { expect(call.max_completion_tokens).toBe(128000) }) + it("omits temperature for Claude Opus 5", async () => { + const handler = new VercelAiGatewayHandler({ + ...mockOptions, + vercelAiGatewayModelId: "anthropic/claude-opus-5", + }) + + await handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]).next() + + // Assert directly on the extracted call arg. `objectContaining({ + // temperature: undefined })` passes whether temperature is explicitly + // undefined or simply absent, so it wouldn't catch a regression where the + // handler stops consulting supportsTemperature. + const call = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(call.model).toBe("anthropic/claude-opus-5") + expect(call.temperature).toBeUndefined() + expect(call.max_completion_tokens).toBe(128000) + }) + it("adds cache breakpoints for supported models", async () => { const { addCacheBreakpoints } = await import("../../transform/caching/vercel-ai-gateway") const handler = new VercelAiGatewayHandler({ diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index f69c6eb29c..b55c8b3089 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -106,6 +106,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "claude-opus-4-6": case "claude-opus-4-7": case "claude-opus-4-8": + case "claude-opus-5": case "claude-fable-5": case "claude-opus-4-5-20251101": case "claude-opus-4-1-20250805": @@ -177,6 +178,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "claude-opus-4-6": case "claude-opus-4-7": case "claude-opus-4-8": + case "claude-opus-5": case "claude-fable-5": case "claude-opus-4-5-20251101": case "claude-opus-4-1-20250805": diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index ae072b3559..2981ed0149 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -301,12 +301,12 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * Detect models that require the adaptive-thinking API contract. * * Starting with Claude Opus 4.7 (and the matching Sonnet 4.7), and continuing - * in Opus 4.8 / Sonnet 4.8, Claude Fable 5, and Claude Sonnet 5, Anthropic - * removed sampling parameters (temperature/top_p/top_k) and replaced + * in Opus 4.8 / Sonnet 4.8, Claude Fable 5, Claude Sonnet 5, and Claude Opus 5, + * Anthropic removed sampling parameters (temperature/top_p/top_k) and replaced * budget_tokens-based thinking with `thinking.type: "adaptive"` plus * `output_config.effort`. The migration guide from 4.7 → 4.8 confirms there - * are no further breaking API changes, and Fable 5 / Sonnet 5 keep the same - * adaptive-thinking contract, so a single guard matches all generations. + * are no further breaking API changes, and Fable 5 / Sonnet 5 / Opus 5 keep the + * same adaptive-thinking contract, so a single guard matches all generations. * Shared by createMessage and completePrompt so both request paths omit * temperature for these models (sending it causes a 400). * @@ -318,6 +318,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH return ( baseModelId.includes("opus-4-7") || baseModelId.includes("opus-4-8") || + baseModelId.includes("opus-5") || baseModelId.includes("fable-5") || baseModelId.includes("sonnet-4-7") || baseModelId.includes("sonnet-4-8") || diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index 7794031fa5..89169c9c56 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -346,6 +346,34 @@ describe("OpenRouter API", () => { expect(result.supportsReasoningBinary).toBe(true) }) + it("sets claude-opus-5 model to Anthropic max tokens and omits temperature", () => { + const mockModel = { + name: "Claude Opus 5", + description: "Test model", + context_length: 1000000, + max_completion_tokens: 128000, + pricing: { + prompt: "0.000005", + completion: "0.000025", + }, + } + + const result = parseOpenRouterModel({ + id: "anthropic/claude-opus-5", + model: mockModel, + inputModality: ["text", "image"], + outputModality: ["text"], + maxTokens: 128000, + supportedParameters: ["reasoning", "include_reasoning"], + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.supportsTemperature).toBe(false) + expect(result.supportsReasoningBudget).toBe(true) + expect(result.supportsReasoningBinary).toBe(true) + }) + it("sets horizon-alpha model to 32k max tokens", () => { const mockModel = { name: "Horizon Alpha", diff --git a/src/api/providers/fetchers/__tests__/requesty.spec.ts b/src/api/providers/fetchers/__tests__/requesty.spec.ts index 89c56063ef..25d622b6ed 100644 --- a/src/api/providers/fetchers/__tests__/requesty.spec.ts +++ b/src/api/providers/fetchers/__tests__/requesty.spec.ts @@ -75,6 +75,31 @@ describe("getRequestyModels", () => { expect(sonnet5.supportsTemperature).toBe(false) }) + it("applies Opus 5 overrides when parsing anthropic/claude-opus-5", async () => { + const rawOpus5 = makeRawModel({ + id: "anthropic/claude-opus-5", + max_output_tokens: 128000, + context_window: 1000000, + supports_caching: true, + supports_vision: true, + supports_reasoning: true, + input_price: "0.000005", + output_price: "0.000025", + caching_price: "0.00000625", + cached_price: "0.0000005", + }) + + mockAxiosGet.mockResolvedValueOnce({ data: { data: [rawOpus5] } }) + + const models = await getRequestyModels() + const opus5 = models["anthropic/claude-opus-5"] + + expect(opus5).toBeDefined() + expect(opus5.supportsReasoningBudget).toBe(true) + expect(opus5.supportsReasoningBinary).toBe(true) + expect(opus5.supportsTemperature).toBe(false) + }) + it("does not apply Fable 5 overrides to other models", async () => { const rawSonnet = makeRawModel({ id: "anthropic/claude-sonnet-4.6", diff --git a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts index 28e08d776b..fa8e89e8d0 100644 --- a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts @@ -237,6 +237,22 @@ describe("Vercel AI Gateway Fetchers", () => { expect(result.supportsTemperature).toBe(false) }) + it("marks Claude Opus 5 as not supporting temperature", () => { + const result = parseVercelAiGatewayModel({ + id: "anthropic/claude-opus-5", + model: { + ...baseModel, + id: "anthropic/claude-opus-5", + context_window: 1000000, + max_tokens: 128000, + }, + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.supportsTemperature).toBe(false) + }) + it("detects vision-only models", () => { // claude 3.5 haiku in VERCEL_AI_GATEWAY_VISION_ONLY_MODELS const visionModel = { diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index 2dcb342445..a25c1b96fa 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -277,6 +277,13 @@ export const parseOpenRouterModel = ({ modelInfo.supportsTemperature = false } + // Set claude-opus-5 model to use the correct Anthropic configuration + if (id === "anthropic/claude-opus-5") { + modelInfo.maxTokens = anthropicModels["claude-opus-5"].maxTokens + modelInfo.supportsReasoningBinary = true + modelInfo.supportsTemperature = false + } + // Ensure correct reasoning handling for Claude Haiku 4.5 on OpenRouter // Use budget control and disable effort-based reasoning fallback if (id === "anthropic/claude-haiku-4.5") { diff --git a/src/api/providers/fetchers/requesty.ts b/src/api/providers/fetchers/requesty.ts index 2260d02978..861fbfb181 100644 --- a/src/api/providers/fetchers/requesty.ts +++ b/src/api/providers/fetchers/requesty.ts @@ -57,6 +57,12 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom modelInfo.supportsTemperature = false } + if (rawModel.id === "anthropic/claude-opus-5") { + modelInfo.supportsReasoningBudget = true + modelInfo.supportsReasoningBinary = true + modelInfo.supportsTemperature = false + } + models[rawModel.id] = modelInfo } } catch (error) { diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts index 1c4a4279e1..41865ebf44 100644 --- a/src/api/providers/fetchers/vercel-ai-gateway.ts +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -124,5 +124,9 @@ export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: Ve modelInfo.supportsTemperature = false } + if (id === "anthropic/claude-opus-5") { + modelInfo.supportsTemperature = false + } + return modelInfo } diff --git a/src/shared/__tests__/api.spec.ts b/src/shared/__tests__/api.spec.ts index f4fb8330bb..0363c27cdf 100644 --- a/src/shared/__tests__/api.spec.ts +++ b/src/shared/__tests__/api.spec.ts @@ -189,6 +189,33 @@ describe("getModelMaxOutputTokens", () => { ).toBe(32_768) }) + test("should preserve Anthropic hybrid token handling for Claude Opus 5", () => { + const model: ModelInfo = { + contextWindow: 1_000_000, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + maxTokens: 128_000, + } + + expect( + getModelMaxOutputTokens({ + modelId: "claude-opus-5", + model, + settings: { apiProvider: "anthropic", enableReasoningEffort: false }, + }), + ).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS) + + expect( + getModelMaxOutputTokens({ + modelId: "claude-opus-5", + model, + settings: { apiProvider: "anthropic", enableReasoningEffort: true, modelMaxTokens: 32_768 }, + }), + ).toBe(32_768) + }) + test("should return model.maxTokens for non-Anthropic models that support reasoning budget but aren't using it", () => { const geminiModelId = "gemini-2.5-flash-preview-04-17" const model: ModelInfo = { From d1f399989be7c24e4e5e8676b8ecd98a686fc9f8 Mon Sep 17 00:00:00 2001 From: Naved Merchant Date: Fri, 24 Jul 2026 19:33:04 -0700 Subject: [PATCH 55/89] Release v3.72.0 (#1013) * chore: prepare v3.72.0 release * fix: address v3.72.0 release review * Update README.md * Update README.md --------- Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- CHANGELOG.md | 27 +++++++++++++++++++ README.md | 19 +++++++------ locales/ca/README.md | 18 ++++++------- locales/de/README.md | 18 ++++++------- locales/es/README.md | 18 ++++++------- locales/fr/README.md | 18 ++++++------- locales/hi/README.md | 18 ++++++------- locales/id/README.md | 18 ++++++------- locales/it/README.md | 18 ++++++------- locales/ja/README.md | 18 ++++++------- locales/ko/README.md | 18 ++++++------- locales/nl/README.md | 18 ++++++------- locales/pl/README.md | 18 ++++++------- locales/pt-BR/README.md | 18 ++++++------- locales/ru/README.md | 18 ++++++------- locales/tr/README.md | 18 ++++++------- locales/vi/README.md | 18 ++++++------- locales/zh-CN/README.md | 18 ++++++------- locales/zh-TW/README.md | 18 ++++++------- src/core/webview/ClineProvider.ts | 2 +- src/package.json | 2 +- .../chat/__tests__/Announcement.spec.tsx | 16 +++++------ webview-ui/src/i18n/locales/ca/chat.json | 6 ++--- webview-ui/src/i18n/locales/de/chat.json | 6 ++--- webview-ui/src/i18n/locales/en/chat.json | 6 ++--- webview-ui/src/i18n/locales/es/chat.json | 6 ++--- webview-ui/src/i18n/locales/fr/chat.json | 6 ++--- webview-ui/src/i18n/locales/hi/chat.json | 6 ++--- webview-ui/src/i18n/locales/id/chat.json | 6 ++--- webview-ui/src/i18n/locales/it/chat.json | 6 ++--- webview-ui/src/i18n/locales/ja/chat.json | 6 ++--- webview-ui/src/i18n/locales/ko/chat.json | 6 ++--- webview-ui/src/i18n/locales/nl/chat.json | 6 ++--- webview-ui/src/i18n/locales/pl/chat.json | 6 ++--- webview-ui/src/i18n/locales/pt-BR/chat.json | 6 ++--- webview-ui/src/i18n/locales/ru/chat.json | 6 ++--- webview-ui/src/i18n/locales/tr/chat.json | 6 ++--- webview-ui/src/i18n/locales/vi/chat.json | 6 ++--- webview-ui/src/i18n/locales/zh-CN/chat.json | 6 ++--- webview-ui/src/i18n/locales/zh-TW/chat.json | 6 ++--- 40 files changed, 253 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433b11aab5..8a96b42574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Zoo Code Changelog +## [3.72.0] + +### Minor Changes + +- Add the Moonshot provider with live model discovery, streaming, model metadata, and a model picker (PR #857 by @grizmin) +- Add the Kimi Code provider with OAuth device-flow authentication (PR #945 by @taltas) +- Add Claude Opus 5 support across all providers (PR #1010 by @app/zoomote) +- Add Kimi K3 to the Moonshot and OpenCode Go providers (#932 by @navedmerchant, PR #996 by @app/zoomote) +- Add Gemini 3.6 Flash model support (PR #975 by @app/zoomote) +- Add MiniMax-M3 model support (#888 by @RayWinter0816, PR #946 by @app/zoomote) +- Add a safe way to abandon interrupted subtasks by severing stale parent-child links and surfacing delegation status (#559 by @edelauna, PR #935 by @edelauna) +- Add Dart support to codebase indexing (#940 by @WebMad, PR #941 by @WebMad) +- Fix codebase indexing for plain-text files (#931 by @tool-buddy, PR #938 by @WebMad) +- Enable image input for DeepSeek V4 models (#964 by @grizmin, PR #963 by @grizmin) +- Fix ChatGPT OAuth requests for GPT-5.6 Luna being rejected by the Codex backend (PR #889 by @taltas) +- Preserve `reasoning_content` for known reasoning model families when using LiteLLM (#891 by @daewoongoh, PR #899 by @daewoongoh) +- Fix task-history cache invalidation races by routing `invalidate()` and `invalidateAll()` through the task-history lock (#698 by @edelauna, PR #912 by @morgan-coded) +- Fix Settings mode changes by synchronizing the local `cachedState` editing buffer (#914 by @easonLiangWorldedtech, PR #925 by @easonLiangWorldedtech) +- Dismiss the welcome screen after successful Zoo Gateway sign-in (#961 by @JohnCanty, PR #962 by @JamesRobert20) +- Add `CompletePromptOptions` to the `completePrompt` API so callers can configure prompt completion (#615 by @edelauna, PR #901 by @easonLiangWorldedtech) +- Centralize provider identifiers into canonical shared types (#951 by @WebMad, PR #952 by @WebMad) +- Refactor provider categories to use canonical provider identifiers (PR #989 by @WebMad) +- Remove obsolete MCP server-creation translations (#895 by @edelauna, PR #943 by @WebMad) +- Add regression coverage for resuming interrupted subtasks (#566 by @myk1yt, PR #911 by @edelauna) +- Upload coverage reports as GitHub Actions artifacts to simplify CI debugging (PR #939 by @app/zoomote) +- Update `esbuild-wasm` to v0.28.1 (PR #829 by @app/renovate) + ## [3.70.0] ### Minor Changes diff --git a/README.md b/README.md index 5343674b7a..c3ff388c97 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,15 @@ You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX) for this exact support, so if you are having problems or if you have question, jump on and ask. -## What's New in v3.70.0 +## What's New in v3.72.0 -- **OpenAI GPT-5.6 family** — `Sol`, `Terra`, and `Luna` are now available across both the OpenAI Codex and OpenAI Native provider paths. -- **Grok 4.5 support** — xAI's new flagship model, plus a reasoning-effort format fix that also benefits Grok 4 Mini. -- **Kenari provider support** — a first-class, OpenAI-compatible AI gateway billed in Rupiah covering Claude, GPT, DeepSeek, GLM, Kimi and more. -- Surface the context-compaction button and context window progress bar in the collapsed task header. -- Fix: terminal output loss and premature task completion on cold terminals. -- Fix: image attach for Zoo Gateway and Vercel AI Gateway models now follows live vision-capability tags instead of a static allowlist. -- Dependency and tooling updates. +- **Moonshot and Kimi Code providers** — discover Moonshot models dynamically or sign in to Kimi Code through its OAuth device flow. +- **Latest model support** — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3. +- **Improved subtask workflows** — abandon interrupted subtasks cleanly, with safer task-history invalidation and better resume coverage. +- **Expanded codebase indexing** — index Dart and plain-text files. +- **Provider reliability** — fixes for GPT-5.6 Luna with ChatGPT OAuth, LiteLLM reasoning content, and DeepSeek V4 image input. +- **Smoother setup and settings** — improved Zoo Gateway sign-in and mode-switch behavior. +- API, provider-type, dependency, localization, and CI improvements.
🌐 Available languages @@ -66,8 +66,7 @@ for this exact support, so if you are having problems or if you have question, j - [Tiếng Việt](locales/vi/README.md) - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) -- ... -
+
--- diff --git a/locales/ca/README.md b/locales/ca/README.md index 1b113deb5e..9853926e20 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -36,15 +36,15 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o algun dubte, entra i pregunta. -## Novetats a la v3.70.0 - -- **Família OpenAI GPT-5.6** — `Sol`, `Terra` i `Luna` ja estan disponibles tant a OpenAI Codex com a OpenAI Native. -- **Suport per a Grok 4.5** — el nou model insígnia de xAI, a més d'una correcció del format de l'esforç de raonament que també beneficia Grok 4 Mini. -- **Suport per al proveïdor Kenari** — una passarel·la d'IA de primer nivell, compatible amb OpenAI, facturada en rupies, que cobreix Claude, GPT, DeepSeek, GLM, Kimi i més. -- Mostra el botó de condensació de context i la barra de progrés de la finestra de context a la capçalera de tasca col·lapsada. -- Correcció: pèrdua de sortida del terminal i finalització prematura de tasques en terminals freds. -- Correcció: l'adjunció d'imatges per a Zoo Gateway i Vercel AI Gateway ara segueix les etiquetes de capacitat de visió en directe en lloc d'una llista blanca estàtica. -- Actualitzacions de dependències i eines. +## Novetats a la v3.72.0 + +- **Proveïdors Moonshot i Kimi Code** — descobreix dinàmicament els models de Moonshot o inicia la sessió a Kimi Code mitjançant el flux de dispositiu OAuth. +- **Compatibilitat amb els models més recents** — utilitza Claude Opus 5 amb tots els proveïdors, a més de Kimi K3, Gemini 3.6 Flash i MiniMax-M3. +- **Fluxos de subtasques millorats** — abandona subtasques interrompudes de manera neta, amb una invalidació més segura de l'historial de tasques i una millor cobertura de represa. +- **Indexació de la base de codi ampliada** — indexa fitxers Dart i de text pla. +- **Fiabilitat dels proveïdors** — correccions per a GPT-5.6 Luna amb ChatGPT OAuth, el contingut de raonament de LiteLLM i l'entrada d'imatges de DeepSeek V4. +- **Configuració més fluida** — millores en l'inici de sessió de Zoo Gateway i en el canvi de mode. +- Millores de l'API, els tipus de proveïdor, les dependències, la localització i la CI. ## Què pot fer Zoo Code per TU? diff --git a/locales/de/README.md b/locales/de/README.md index 5a0d2950e8..0706f8d992 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -36,15 +36,15 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Roo→Zoo-Migrationsleitfaden](https://docs.zoocode.dev/roo-to-zoo-migration). Wir wollen Nutzer beim Umstieg so gut wie möglich unterstützen, und genau dafür sind unser [Reddit](https://www.reddit.com/r/ZooCode) und [Discord](https://discord.gg/VxfP4Vx3gX) da. Wenn du Probleme hast oder Fragen auftauchen, komm vorbei und frag nach. -## Neu in v3.70.0 - -- **OpenAI GPT-5.6-Familie** — `Sol`, `Terra` und `Luna` sind jetzt sowohl über OpenAI Codex als auch über OpenAI Native verfügbar. -- **Grok 4.5-Unterstützung** — xAIs neues Flaggschiff-Modell, plus ein Fix für das Reasoning-Effort-Format, von dem auch Grok 4 Mini profitiert. -- **Kenari-Anbieter-Unterstützung** — ein erstklassiges, OpenAI-kompatibles KI-Gateway mit Abrechnung in Rupiah, das Claude, GPT, DeepSeek, GLM, Kimi und mehr abdeckt. -- Zeige den Kontext-Kondensierungs-Button und den Fortschrittsbalken für das Kontextfenster im eingeklappten Task-Header an. -- Fix: Verlust von Terminal-Ausgaben und verfrühter Task-Abschluss bei kalten Terminals. -- Fix: Bild-Anhänge für Zoo Gateway und Vercel AI Gateway folgen jetzt den aktuellen Vision-Fähigkeits-Tags statt einer statischen Positivliste. -- Updates für Abhängigkeiten und Tooling. +## Neu in v3.72.0 + +- **Moonshot- und Kimi-Code-Anbieter** — erkenne Moonshot-Modelle dynamisch oder melde dich über den OAuth-Gerätefluss bei Kimi Code an. +- **Unterstützung für die neuesten Modelle** — nutze Claude Opus 5 anbieterübergreifend sowie Kimi K3, Gemini 3.6 Flash und MiniMax-M3. +- **Verbesserte Unteraufgaben-Workflows** — brich unterbrochene Unteraufgaben sauber ab, mit sichererer Invalidierung des Aufgabenverlaufs und besserer Abdeckung beim Fortsetzen. +- **Erweiterte Codebasis-Indizierung** — indiziere Dart- und Klartextdateien. +- **Zuverlässigere Anbieter** — Korrekturen für GPT-5.6 Luna mit ChatGPT OAuth, LiteLLM-Reasoning-Inhalte und Bildeingaben für DeepSeek V4. +- **Reibungslosere Einrichtung und Einstellungen** — verbessertes Zoo-Gateway-Login und Moduswechselverhalten. +- Verbesserungen an API, Anbietertypen, Abhängigkeiten, Lokalisierung und CI. ## Was kann Zoo Code für DICH tun? diff --git a/locales/es/README.md b/locales/es/README.md index 5015784888..e8e67aec72 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -36,15 +36,15 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guía de migración Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ayudar a los usuarios durante la transición, y para eso tenemos nuestro [Reddit](https://www.reddit.com/r/ZooCode) y [Discord](https://discord.gg/VxfP4Vx3gX). Si tienes problemas o alguna pregunta, entra y pregúntanos. -## Novedades de la v3.70.0 - -- **Familia OpenAI GPT-5.6** — `Sol`, `Terra` y `Luna` ya están disponibles tanto en OpenAI Codex como en OpenAI Native. -- **Soporte para Grok 4.5** — el nuevo modelo insignia de xAI, además de una corrección del formato de esfuerzo de razonamiento que también beneficia a Grok 4 Mini. -- **Soporte para el proveedor Kenari** — una pasarela de IA de primer nivel, compatible con OpenAI, facturada en rupias, que cubre Claude, GPT, DeepSeek, GLM, Kimi y más. -- Muestra el botón de condensación de contexto y la barra de progreso de la ventana de contexto en el encabezado de tarea colapsado. -- Corrección: pérdida de salida de terminal y finalización prematura de tareas en terminales fríos. -- Corrección: la adjunción de imágenes para Zoo Gateway y Vercel AI Gateway ahora sigue las etiquetas de capacidad de visión en vivo en lugar de una lista blanca estática. -- Actualizaciones de dependencias y herramientas. +## Novedades de la v3.72.0 + +- **Proveedores Moonshot y Kimi Code** — descubre dinámicamente los modelos de Moonshot o inicia sesión en Kimi Code mediante su flujo de dispositivo OAuth. +- **Compatibilidad con los modelos más recientes** — usa Claude Opus 5 con todos los proveedores, además de Kimi K3, Gemini 3.6 Flash y MiniMax-M3. +- **Flujos de subtareas mejorados** — abandona subtareas interrumpidas de forma limpia, con una invalidación más segura del historial de tareas y una mejor cobertura de reanudación. +- **Indexación ampliada de la base de código** — indexa archivos Dart y de texto sin formato. +- **Fiabilidad de los proveedores** — correcciones para GPT-5.6 Luna con ChatGPT OAuth, el contenido de razonamiento de LiteLLM y la entrada de imágenes de DeepSeek V4. +- **Configuración y ajustes más fluidos** — mejoras en el inicio de sesión de Zoo Gateway y en el cambio de modo. +- Mejoras en la API, los tipos de proveedor, las dependencias, la localización y la CI. ## ¿Qué puede hacer Zoo Code por TI? diff --git a/locales/fr/README.md b/locales/fr/README.md index cbc318e898..49ad6618f5 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -36,15 +36,15 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [guide de migration Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). On veut aider au maximum pendant la transition, et notre [Reddit](https://www.reddit.com/r/ZooCode) et notre [Discord](https://discord.gg/VxfP4Vx3gX) sont là pour ça. Si tu rencontres un problème ou si tu as une question, viens demander. -## Nouveautés de la v3.70.0 - -- **Famille OpenAI GPT-5.6** — `Sol`, `Terra` et `Luna` sont désormais disponibles à la fois via OpenAI Codex et OpenAI Native. -- **Prise en charge de Grok 4.5** — le nouveau modèle phare de xAI, ainsi qu'une correction du format de l'effort de raisonnement qui profite aussi à Grok 4 Mini. -- **Prise en charge du fournisseur Kenari** — une passerelle IA de premier ordre, compatible OpenAI, facturée en roupies, couvrant Claude, GPT, DeepSeek, GLM, Kimi et plus encore. -- Affiche le bouton de condensation du contexte et la barre de progression de la fenêtre de contexte dans l'en-tête de tâche réduit. -- Correctif : perte de sortie du terminal et fin de tâche prématurée sur des terminaux froids. -- Correctif : l'ajout d'images pour Zoo Gateway et Vercel AI Gateway suit désormais les balises de capacité de vision en direct au lieu d'une liste blanche statique. -- Mises à jour des dépendances et des outils. +## Nouveautés de la v3.72.0 + +- **Providers Moonshot et Kimi Code** — découvre dynamiquement les modèles Moonshot ou connecte-toi à Kimi Code via son flux d'appareil OAuth. +- **Prise en charge des derniers modèles** — utilise Claude Opus 5 avec tous les providers, ainsi que Kimi K3, Gemini 3.6 Flash et MiniMax-M3. +- **Workflows de sous-tâches améliorés** — abandonne proprement les sous-tâches interrompues, avec une invalidation plus sûre de l'historique et une meilleure couverture de reprise. +- **Indexation étendue de la base de code** — indexe les fichiers Dart et texte brut. +- **Fiabilité des providers** — correctifs pour GPT-5.6 Luna avec ChatGPT OAuth, le contenu de raisonnement LiteLLM et les images DeepSeek V4. +- **Configuration et réglages plus fluides** — connexion à Zoo Gateway et changement de mode améliorés. +- Améliorations de l'API, des types de providers, des dépendances, de la localisation et de la CI. ## Que peut faire Zoo Code pour VOUS ? diff --git a/locales/hi/README.md b/locales/hi/README.md index 745b5d7540..10a442daf5 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -35,15 +35,15 @@ Roo Code से Zoo Code में आने के लिए एक quick guide तुम्हें [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration) में मिल जाएगी। We plan to help users as much as possible during the transition, और उसी support के लिए हमारा [Reddit](https://www.reddit.com/r/ZooCode) और [Discord](https://discord.gg/VxfP4Vx3gX) है। अगर तुम्हें कोई problem हो या कोई question हो, आकर पूछो। -## v3.70.0 में नया क्या है - -- **OpenAI GPT-5.6 family** — `Sol`, `Terra`, और `Luna` अब OpenAI Codex और OpenAI Native दोनों provider paths पर उपलब्ध हैं। -- **Grok 4.5 समर्थन** — xAI का नया flagship model, साथ ही एक reasoning-effort format fix जिसका फायदा Grok 4 Mini को भी मिलता है। -- **Kenari provider समर्थन** — एक first-class, OpenAI-compatible AI gateway जो Rupiah में बिल होता है और Claude, GPT, DeepSeek, GLM, Kimi और अधिक को cover करता है। -- Collapsed task header में context-compaction button और context window progress bar दिखाएं। -- फिक्स: cold terminals पर terminal output loss और समय से पहले task completion। -- फिक्स: Zoo Gateway और Vercel AI Gateway models के लिए image attach अब static allowlist के बजाय live vision-capability tags को follow करता है। -- Dependency और tooling अपडेट्स। +## v3.72.0 में नया क्या है + +- **Moonshot और Kimi Code providers** — Moonshot models को dynamically खोजें या OAuth device flow से Kimi Code में sign in करें। +- **नवीनतम model support** — providers पर Claude Opus 5 के साथ Kimi K3, Gemini 3.6 Flash और MiniMax-M3 का उपयोग करें। +- **बेहतर subtask workflows** — interrupted subtasks को साफ़ तौर पर छोड़ें, अधिक सुरक्षित task-history invalidation और बेहतर resume coverage के साथ। +- **विस्तारित codebase indexing** — Dart और plain-text files को index करें। +- **Provider reliability** — ChatGPT OAuth के साथ GPT-5.6 Luna, LiteLLM reasoning content और DeepSeek V4 image input के लिए fixes। +- **अधिक सहज setup और settings** — Zoo Gateway sign-in और mode-switch behavior में सुधार। +- API, provider types, dependencies, localization और CI में सुधार। ## Zoo Code आपके लिए क्या कर सकता है? diff --git a/locales/id/README.md b/locales/id/README.md index a2f995688d..c2b0139290 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -35,15 +35,15 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di [panduan migrasi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Kami ingin membantu pengguna semaksimal mungkin selama masa transisi, dan itulah gunanya [Reddit](https://www.reddit.com/r/ZooCode) dan [Discord](https://discord.gg/VxfP4Vx3gX) kami. Kalau kamu mengalami masalah atau punya pertanyaan, langsung mampir dan tanya. -## Yang Baru di v3.70.0 - -- **Keluarga OpenAI GPT-5.6** — `Sol`, `Terra`, dan `Luna` kini tersedia lewat jalur provider OpenAI Codex maupun OpenAI Native. -- **Dukungan Grok 4.5** — model unggulan baru dari xAI, plus perbaikan format reasoning-effort yang juga bermanfaat untuk Grok 4 Mini. -- **Dukungan provider Kenari** — AI gateway kelas satu yang kompatibel dengan OpenAI, ditagih dalam Rupiah, mencakup Claude, GPT, DeepSeek, GLM, Kimi, dan lainnya. -- Menampilkan tombol pemadatan konteks dan progress bar jendela konteks di header tugas yang diciutkan. -- Perbaikan: hilangnya output terminal dan penyelesaian tugas prematur pada terminal yang dingin (cold terminal). -- Perbaikan: attach gambar untuk model Zoo Gateway dan Vercel AI Gateway kini mengikuti tag kemampuan vision secara langsung, bukan allowlist statis. -- Pembaruan dependensi dan tooling. +## Yang Baru di v3.72.0 + +- **Provider Moonshot dan Kimi Code** — temukan model Moonshot secara dinamis atau masuk ke Kimi Code melalui alur perangkat OAuth. +- **Dukungan model terbaru** — gunakan Claude Opus 5 di berbagai provider, ditambah Kimi K3, Gemini 3.6 Flash, dan MiniMax-M3. +- **Workflow subtask yang lebih baik** — tinggalkan subtask yang terinterupsi dengan bersih, dengan invalidasi riwayat tugas yang lebih aman dan cakupan resume yang lebih baik. +- **Indexing codebase yang lebih luas** — indeks file Dart dan teks biasa. +- **Keandalan provider** — perbaikan untuk GPT-5.6 Luna dengan ChatGPT OAuth, reasoning content LiteLLM, dan input gambar DeepSeek V4. +- **Setup dan pengaturan lebih mulus** — peningkatan login Zoo Gateway dan perilaku pergantian mode. +- Peningkatan API, tipe provider, dependensi, lokalisasi, dan CI. ## Apa yang Bisa Zoo Code Lakukan Untuk ANDA? diff --git a/locales/it/README.md b/locales/it/README.md index 7196b90812..1dd8287b49 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -36,15 +36,15 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida alla migrazione Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Vogliamo aiutare gli utenti il più possibile durante la transizione, e per questo abbiamo il nostro [Reddit](https://www.reddit.com/r/ZooCode) e il nostro [Discord](https://discord.gg/VxfP4Vx3gX). Se hai problemi o domande, passa pure e chiedi. -## Novità in v3.70.0 - -- **Famiglia OpenAI GPT-5.6** — `Sol`, `Terra` e `Luna` sono ora disponibili sia tramite OpenAI Codex sia tramite OpenAI Native. -- **Supporto Grok 4.5** — il nuovo modello di punta di xAI, oltre a una correzione del formato reasoning-effort che avvantaggia anche Grok 4 Mini. -- **Supporto provider Kenari** — un AI gateway di prim'ordine, compatibile con OpenAI, fatturato in Rupie, che copre Claude, GPT, DeepSeek, GLM, Kimi e altro ancora. -- Mostra il pulsante di condensazione del contesto e la barra di avanzamento della finestra di contesto nell'intestazione dell'attività compressa. -- Correzione: perdita di output del terminale e completamento prematuro delle attività su terminali freddi. -- Correzione: l'allegato di immagini per i modelli Zoo Gateway e Vercel AI Gateway ora segue i tag di capacità di visione in tempo reale invece di una allowlist statica. -- Aggiornamenti di dipendenze e tooling. +## Novità in v3.72.0 + +- **Provider Moonshot e Kimi Code** — rileva dinamicamente i modelli Moonshot oppure accedi a Kimi Code tramite il flusso dispositivo OAuth. +- **Supporto per i modelli più recenti** — usa Claude Opus 5 con tutti i provider, oltre a Kimi K3, Gemini 3.6 Flash e MiniMax-M3. +- **Workflow delle sottoattività migliorati** — abbandona in modo pulito le sottoattività interrotte, con un'invalidazione più sicura della cronologia e una migliore copertura della ripresa. +- **Indicizzazione della codebase ampliata** — indicizza i file Dart e di testo semplice. +- **Affidabilità dei provider** — correzioni per GPT-5.6 Luna con ChatGPT OAuth, il reasoning content di LiteLLM e l'input di immagini di DeepSeek V4. +- **Configurazione e impostazioni più fluide** — accesso a Zoo Gateway e cambio modalità migliorati. +- Miglioramenti ad API, tipi di provider, dipendenze, localizzazione e CI. ## Cosa può fare Zoo Code per TE? diff --git a/locales/ja/README.md b/locales/ja/README.md index 47ccf716d0..08f4ce1b95 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -35,15 +35,15 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo→Zoo 移行ガイド](https://docs.zoocode.dev/roo-to-zoo-migration) で確認できます。移行中のユーザーをできるだけ支援したいと考えていて、そのために [Reddit](https://www.reddit.com/r/ZooCode) と [Discord](https://discord.gg/VxfP4Vx3gX) を用意しています。困ったことや質問があれば、気軽に参加して聞いてください。 -## v3.70.0 の新機能 - -- **OpenAI GPT-5.6 ファミリー** — `Sol`、`Terra`、`Luna` が OpenAI Codex と OpenAI Native の両方のプロバイダーパスで利用可能になりました。 -- **Grok 4.5 サポート** — xAI の新しいフラッグシップモデルに加え、Grok 4 Mini にも恩恵のある reasoning-effort フォーマット修正。 -- **Kenari プロバイダーサポート** — Claude、GPT、DeepSeek、GLM、Kimi などをカバーする、ルピア建てのファーストクラスの OpenAI 互換 AI ゲートウェイ。 -- 折りたたまれたタスクヘッダーにコンテキスト圧縮ボタンとコンテキストウィンドウの進捗バーを表示。 -- 修正: コールドターミナルでのターミナル出力の消失と早すぎるタスク完了。 -- 修正: Zoo Gateway と Vercel AI Gateway モデルの画像添付が、静的な許可リストではなくライブの vision 対応タグに従うように。 -- 依存関係とツールの更新。 +## v3.72.0 の新機能 + +- **Moonshot と Kimi Code プロバイダー** — Moonshot モデルを動的に検出するか、OAuth デバイスフローで Kimi Code にサインインできます。 +- **最新モデルのサポート** — 各プロバイダーで Claude Opus 5 を利用できるほか、Kimi K3、Gemini 3.6 Flash、MiniMax-M3 にも対応しました。 +- **サブタスクワークフローの改善** — タスク履歴をより安全に無効化し、再開時のテスト範囲を強化しながら、中断されたサブタスクを安全に破棄できます。 +- **コードベースインデックスの拡張** — Dart ファイルとプレーンテキストファイルをインデックスできます。 +- **プロバイダーの信頼性向上** — ChatGPT OAuth での GPT-5.6 Luna、LiteLLM の reasoning content、DeepSeek V4 の画像入力を修正しました。 +- **よりスムーズなセットアップと設定** — Zoo Gateway のサインインとモード切り替えを改善しました。 +- API、プロバイダー型、依存関係、ローカライズ、CI を改善しました。 ## Zoo Codeがあなたのためにできること diff --git a/locales/ko/README.md b/locales/ko/README.md index a5c41b6863..2d8441b101 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -34,15 +34,15 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그레이션 가이드](https://docs.zoocode.dev/roo-to-zoo-migration)에서 확인할 수 있어. 전환하는 동안 사용자들을 최대한 돕고 싶고, 바로 그 지원을 위해 [Reddit](https://www.reddit.com/r/ZooCode)와 [Discord](https://discord.gg/VxfP4Vx3gX)를 운영하고 있어. 문제가 있거나 궁금한 점이 있으면 들어와서 편하게 물어봐. -## v3.70.0의 새로운 기능 - -- **OpenAI GPT-5.6 패밀리** — `Sol`, `Terra`, `Luna`가 이제 OpenAI Codex와 OpenAI Native 프로바이더 경로 모두에서 사용 가능합니다. -- **Grok 4.5 지원** — xAI의 새로운 플래그십 모델과, Grok 4 Mini에도 도움이 되는 reasoning-effort 형식 수정. -- **Kenari 프로바이더 지원** — Claude, GPT, DeepSeek, GLM, Kimi 등을 지원하며 루피아로 청구되는 일류 OpenAI 호환 AI 게이트웨이. -- 접힌 작업 헤더에 컨텍스트 압축 버튼과 컨텍스트 윈도우 진행률 표시줄을 표시합니다. -- 수정: 콜드 터미널에서의 터미널 출력 손실 및 조기 작업 완료 문제. -- 수정: Zoo Gateway 및 Vercel AI Gateway 모델의 이미지 첨부가 이제 정적 허용 목록 대신 실시간 vision 지원 태그를 따릅니다. -- 의존성 및 툴링 업데이트. +## v3.72.0의 새로운 기능 + +- **Moonshot 및 Kimi Code 프로바이더** — Moonshot 모델을 동적으로 검색하거나 OAuth 기기 흐름으로 Kimi Code에 로그인하세요. +- **최신 모델 지원** — 여러 프로바이더에서 Claude Opus 5를 사용하고 Kimi K3, Gemini 3.6 Flash, MiniMax-M3도 이용하세요. +- **개선된 하위 작업 워크플로우** — 더 안전한 작업 기록 무효화와 향상된 재개 테스트 범위로 중단된 하위 작업을 깔끔하게 포기하세요. +- **확장된 코드베이스 인덱싱** — Dart 및 일반 텍스트 파일을 인덱싱하세요. +- **프로바이더 안정성** — ChatGPT OAuth의 GPT-5.6 Luna, LiteLLM reasoning content, DeepSeek V4 이미지 입력 문제를 수정했습니다. +- **더 원활한 설정** — Zoo Gateway 로그인과 모드 전환 동작을 개선했습니다. +- API, 프로바이더 유형, 의존성, 현지화 및 CI 개선. ## Zoo Code가 당신을 위해 무엇을 할 수 있을까요? diff --git a/locales/nl/README.md b/locales/nl/README.md index 7ed26a5495..6ce7a2febb 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -36,15 +36,15 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de [Roo→Zoo-migratiegids](https://docs.zoocode.dev/roo-to-zoo-migration). We willen gebruikers zo goed mogelijk helpen tijdens de overgang, en precies daarvoor zijn onze [Reddit](https://www.reddit.com/r/ZooCode) en [Discord](https://discord.gg/VxfP4Vx3gX) er. Als je ergens tegenaan loopt of vragen hebt, kom langs en vraag het. -## Nieuw in v3.70.0 - -- **OpenAI GPT-5.6-familie** — `Sol`, `Terra` en `Luna` zijn nu beschikbaar via zowel OpenAI Codex als OpenAI Native. -- **Grok 4.5-ondersteuning** — het nieuwe vlaggenschipmodel van xAI, plus een fix voor het reasoning-effort-formaat waar ook Grok 4 Mini baat bij heeft. -- **Kenari-providerondersteuning** — een eersteklas, OpenAI-compatibele AI-gateway die in Roepia factureert en Claude, GPT, DeepSeek, GLM, Kimi en meer dekt. -- Toon de contextcompressie-knop en de voortgangsbalk van het contextvenster in de ingeklapte taakheader. -- Fix: verlies van terminaluitvoer en voortijdige taakvoltooiing bij koude terminals. -- Fix: afbeeldingen bijvoegen voor Zoo Gateway- en Vercel AI Gateway-modellen volgt nu live vision-capaciteitstags in plaats van een statische allowlist. -- Updates van dependencies en tooling. +## Nieuw in v3.72.0 + +- **Moonshot- en Kimi Code-providers** — ontdek Moonshot-modellen dynamisch of meld je aan bij Kimi Code via de OAuth-apparaatstroom. +- **Ondersteuning voor de nieuwste modellen** — gebruik Claude Opus 5 bij verschillende providers, plus Kimi K3, Gemini 3.6 Flash en MiniMax-M3. +- **Verbeterde subtaakworkflows** — breek onderbroken subtaken netjes af, met veiligere invalidatie van taakgeschiedenis en betere hervattingsdekking. +- **Uitgebreide codebase-indexering** — indexeer Dart- en plattetekstbestanden. +- **Betrouwbaardere providers** — fixes voor GPT-5.6 Luna met ChatGPT OAuth, LiteLLM-reasoningcontent en DeepSeek V4-beeldinvoer. +- **Soepelere installatie en instellingen** — verbeterde Zoo Gateway-aanmelding en moduswisseling. +- Verbeteringen aan API, providertypen, dependencies, lokalisatie en CI. ## Wat kan Zoo Code voor JOU doen? diff --git a/locales/pl/README.md b/locales/pl/README.md index cc5473b330..83b898eb13 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -34,15 +34,15 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku migracji Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chcemy jak najlepiej pomagać użytkownikom w czasie przejścia i właśnie do tego służą nasze [Reddit](https://www.reddit.com/r/ZooCode) oraz [Discord](https://discord.gg/VxfP4Vx3gX). Jeśli masz problem albo pytanie, wpadaj i pytaj. -## Nowości w v3.70.0 - -- **Rodzina OpenAI GPT-5.6** — `Sol`, `Terra` i `Luna` są teraz dostępne zarówno w ścieżce providera OpenAI Codex, jak i OpenAI Native. -- **Obsługa Grok 4.5** — nowy flagowy model xAI, plus poprawka formatu reasoning-effort, z której korzysta też Grok 4 Mini. -- **Obsługa providera Kenari** — pierwszorzędna, kompatybilna z OpenAI bramka AI rozliczana w rupiach, obejmująca Claude, GPT, DeepSeek, GLM, Kimi i inne. -- Wyświetlanie przycisku kondensacji kontekstu i paska postępu okna kontekstu w zwiniętym nagłówku zadania. -- Poprawka: utrata danych wyjściowych terminala i przedwczesne kończenie zadań na zimnych terminalach. -- Poprawka: dołączanie obrazów dla modeli Zoo Gateway i Vercel AI Gateway korzysta teraz z bieżących tagów możliwości vision zamiast statycznej listy dozwolonych. -- Aktualizacje zależności i narzędzi. +## Nowości w v3.72.0 + +- **Providerzy Moonshot i Kimi Code** — dynamicznie wykrywaj modele Moonshot lub zaloguj się do Kimi Code przez przepływ urządzenia OAuth. +- **Obsługa najnowszych modeli** — korzystaj z Claude Opus 5 u różnych providerów, a także z Kimi K3, Gemini 3.6 Flash i MiniMax-M3. +- **Ulepszone workflow podzadań** — bezpiecznie porzucaj przerwane podzadania dzięki bezpieczniejszej invalidacji historii i lepszemu pokryciu wznawiania. +- **Rozszerzone indeksowanie bazy kodu** — indeksuj pliki Dart i pliki tekstowe. +- **Niezawodność providerów** — poprawki dla GPT-5.6 Luna z ChatGPT OAuth, reasoning content LiteLLM i obrazów DeepSeek V4. +- **Płynniejsza konfiguracja i ustawienia** — ulepszone logowanie do Zoo Gateway i przełączanie trybów. +- Ulepszenia API, typów providerów, zależności, lokalizacji i CI. ## Co Zoo Code może zrobić dla CIEBIE? diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 90f94199cb..931255ae80 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -36,15 +36,15 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia de migração Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ajudar os usuários durante essa transição da melhor forma possível, e é exatamente para isso que temos nosso [Reddit](https://www.reddit.com/r/ZooCode) e nosso [Discord](https://discord.gg/VxfP4Vx3gX). Se você tiver algum problema ou dúvida, apareça por lá e pergunte. -## Novidades na v3.70.0 - -- **Família OpenAI GPT-5.6** — `Sol`, `Terra` e `Luna` já estão disponíveis tanto pelo OpenAI Codex quanto pelo OpenAI Native. -- **Suporte a Grok 4.5** — o novo modelo principal da xAI, além de uma correção no formato de reasoning-effort que também beneficia o Grok 4 Mini. -- **Suporte ao provedor Kenari** — um gateway de IA de primeira linha, compatível com OpenAI, cobrado em rupias, cobrindo Claude, GPT, DeepSeek, GLM, Kimi e mais. -- Exibe o botão de condensação de contexto e a barra de progresso da janela de contexto no cabeçalho de tarefa recolhido. -- Correção: perda de saída do terminal e conclusão prematura de tarefas em terminais frios. -- Correção: o anexo de imagens para os modelos Zoo Gateway e Vercel AI Gateway agora segue as tags de capacidade de visão em tempo real, em vez de uma allowlist estática. -- Atualizações de dependências e ferramentas. +## Novidades na v3.72.0 + +- **Providers Moonshot e Kimi Code** — descubra dinamicamente os modelos Moonshot ou entre no Kimi Code pelo fluxo de dispositivo OAuth. +- **Suporte aos modelos mais recentes** — use Claude Opus 5 em todos os providers, além de Kimi K3, Gemini 3.6 Flash e MiniMax-M3. +- **Workflows de subtarefas aprimorados** — abandone subtarefas interrompidas de forma limpa, com invalidação mais segura do histórico e melhor cobertura de retomada. +- **Indexação ampliada da base de código** — indexe arquivos Dart e de texto simples. +- **Confiabilidade dos providers** — correções para GPT-5.6 Luna com ChatGPT OAuth, reasoning content do LiteLLM e entrada de imagens do DeepSeek V4. +- **Configuração e ajustes mais fluidos** — melhorias no login do Zoo Gateway e na troca de modo. +- Melhorias em API, tipos de provider, dependências, localização e CI. ## O que o Zoo Code pode fazer por VOCÊ? diff --git a/locales/ru/README.md b/locales/ru/README.md index 35e2541ce8..4d1c716985 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -36,15 +36,15 @@ Короткое руководство по переходу с Roo Code на Zoo Code можно найти в [гайде по миграции Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Мы хотим как можно лучше помочь пользователям во время перехода, и именно для этого у нас есть [Reddit](https://www.reddit.com/r/ZooCode) и [Discord](https://discord.gg/VxfP4Vx3gX). Если у тебя возникнут проблемы или вопросы, заходи и спрашивай. -## Что нового в v3.70.0 - -- **Семейство OpenAI GPT-5.6** — `Sol`, `Terra` и `Luna` теперь доступны как через OpenAI Codex, так и через OpenAI Native. -- **Поддержка Grok 4.5** — новая флагманская модель xAI, а также исправление формата reasoning-effort, от которого также выигрывает Grok 4 Mini. -- **Поддержка провайдера Kenari** — первоклассный, совместимый с OpenAI ИИ-шлюз с оплатой в рупиях, охватывающий Claude, GPT, DeepSeek, GLM, Kimi и другие модели. -- Отображение кнопки сжатия контекста и индикатора заполнения контекстного окна в свёрнутом заголовке задачи. -- Исправление: потеря вывода терминала и преждевременное завершение задачи на «холодных» терминалах. -- Исправление: прикрепление изображений для моделей Zoo Gateway и Vercel AI Gateway теперь следует актуальным тегам поддержки vision вместо статического allowlist. -- Обновления зависимостей и инструментов. +## Что нового в v3.72.0 + +- **Провайдеры Moonshot и Kimi Code** — динамически находите модели Moonshot или входите в Kimi Code через OAuth-поток для устройств. +- **Поддержка новейших моделей** — используйте Claude Opus 5 у всех провайдеров, а также Kimi K3, Gemini 3.6 Flash и MiniMax-M3. +- **Улучшенные рабочие процессы подзадач** — безопасно завершайте прерванные подзадачи благодаря более надёжной инвалидации истории и расширенному тестированию возобновления. +- **Расширенная индексация кодовой базы** — индексируйте файлы Dart и обычные текстовые файлы. +- **Надёжность провайдеров** — исправления GPT-5.6 Luna с ChatGPT OAuth, reasoning content LiteLLM и ввода изображений DeepSeek V4. +- **Более удобные настройки** — улучшены доступ к Zoo Gateway и переключение режимов. +- Улучшения API, типов провайдеров, зависимостей, локализации и CI. ## Что Zoo Code может сделать для ВАС? diff --git a/locales/tr/README.md b/locales/tr/README.md index 3dd0413340..6b2e8a7f4e 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -36,15 +36,15 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da soruların olursa gel ve sor. -## v3.70.0'daki Yenilikler - -- **OpenAI GPT-5.6 ailesi** — `Sol`, `Terra` ve `Luna` artık hem OpenAI Codex hem de OpenAI Native sağlayıcı yollarında kullanılabiliyor. -- **Grok 4.5 desteği** — xAI'nin yeni amiral gemisi modeli, ayrıca Grok 4 Mini'ye de fayda sağlayan bir reasoning-effort format düzeltmesi. -- **Kenari sağlayıcı desteği** — Claude, GPT, DeepSeek, GLM, Kimi ve daha fazlasını kapsayan, Rupi ile faturalandırılan, OpenAI uyumlu, birinci sınıf bir AI ağ geçidi. -- Daraltılmış görev başlığında bağlam yoğunlaştırma düğmesi ve bağlam penceresi ilerleme çubuğunu göster. -- Düzeltme: soğuk terminallerde terminal çıktısı kaybı ve erken görev tamamlanması. -- Düzeltme: Zoo Gateway ve Vercel AI Gateway modelleri için görsel ekleme artık statik bir izin listesi yerine canlı görme yeteneği etiketlerini takip ediyor. -- Bağımlılık ve araç güncellemeleri. +## v3.72.0'daki Yenilikler + +- **Moonshot ve Kimi Code provider'ları** — Moonshot modellerini dinamik olarak keşfet veya OAuth cihaz akışıyla Kimi Code'da oturum aç. +- **En yeni model desteği** — provider'lar genelinde Claude Opus 5'in yanı sıra Kimi K3, Gemini 3.6 Flash ve MiniMax-M3'ü kullan. +- **Geliştirilmiş alt görev iş akışları** — daha güvenli görev geçmişi geçersiz kılma ve daha iyi devam ettirme kapsamıyla kesintiye uğramış alt görevleri temiz biçimde bırak. +- **Genişletilmiş kod tabanı indeksleme** — Dart ve düz metin dosyalarını indeksle. +- **Provider güvenilirliği** — ChatGPT OAuth ile GPT-5.6 Luna, LiteLLM reasoning content ve DeepSeek V4 görsel girdisi için düzeltmeler. +- **Daha akıcı kurulum ve ayarlar** — Zoo Gateway oturum açma ve mod değiştirme davranışı iyileştirildi. +- API, provider türleri, bağımlılıklar, yerelleştirme ve CI iyileştirmeleri. ## Zoo Code SİZİN İçin Ne Yapabilir? diff --git a/locales/vi/README.md b/locales/vi/README.md index 86740a8795..10e493d789 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -35,15 +35,15 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo Code trong [hướng dẫn chuyển đổi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chúng tôi muốn hỗ trợ người dùng nhiều nhất có thể trong quá trình chuyển đổi, và đó chính là lý do chúng tôi có [Reddit](https://www.reddit.com/r/ZooCode) và [Discord](https://discord.gg/VxfP4Vx3gX). Nếu bạn gặp vấn đề hoặc có câu hỏi, cứ vào hỏi nhé. -## Điểm mới trong v3.70.0 - -- **Dòng OpenAI GPT-5.6** — `Sol`, `Terra`, và `Luna` hiện đã có sẵn trên cả hai đường nhà cung cấp OpenAI Codex và OpenAI Native. -- **Hỗ trợ Grok 4.5** — model chủ lực mới của xAI, cùng với bản sửa lỗi định dạng reasoning-effort cũng mang lại lợi ích cho Grok 4 Mini. -- **Hỗ trợ nhà cung cấp Kenari** — một cổng AI hạng nhất, tương thích OpenAI, tính phí bằng Rupiah, bao phủ Claude, GPT, DeepSeek, GLM, Kimi và nhiều hơn nữa. -- Hiển thị nút cô đọng ngữ cảnh và thanh tiến trình cửa sổ ngữ cảnh trong tiêu đề tác vụ đã thu gọn. -- Sửa lỗi: mất đầu ra terminal và hoàn thành tác vụ quá sớm trên các terminal nguội. -- Sửa lỗi: đính kèm hình ảnh cho các model Zoo Gateway và Vercel AI Gateway giờ đây tuân theo các thẻ khả năng vision trực tiếp thay vì danh sách cho phép tĩnh. -- Cập nhật phụ thuộc và công cụ. +## Điểm mới trong v3.72.0 + +- **Provider Moonshot và Kimi Code** — khám phá động các model Moonshot hoặc đăng nhập Kimi Code qua luồng thiết bị OAuth. +- **Hỗ trợ model mới nhất** — dùng Claude Opus 5 trên tất cả provider, cùng với Kimi K3, Gemini 3.6 Flash và MiniMax-M3. +- **Workflow tác vụ con được cải thiện** — từ bỏ gọn gàng các tác vụ con bị gián đoạn, với cơ chế vô hiệu hóa lịch sử an toàn hơn và phạm vi kiểm thử tiếp tục tốt hơn. +- **Mở rộng lập chỉ mục codebase** — lập chỉ mục file Dart và file văn bản thuần. +- **Độ tin cậy của provider** — sửa lỗi GPT-5.6 Luna với ChatGPT OAuth, reasoning content của LiteLLM và đầu vào hình ảnh DeepSeek V4. +- **Thiết lập và cài đặt mượt mà hơn** — cải thiện đăng nhập Zoo Gateway và hành vi chuyển chế độ. +- Cải tiến API, kiểu provider, phụ thuộc, bản địa hóa và CI. ## Zoo Code có thể làm gì cho BẠN? diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 9a3e44c155..4bc89bacb4 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -32,15 +32,15 @@ 你可以在 [Roo→Zoo 迁移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到从 Roo Code 迁移到 Zoo Code 的快速说明。我们希望在大家迁移过程中尽可能提供帮助,这也是我们设立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社区的原因。如果你遇到问题或有任何疑问,欢迎加入后直接提问。 -## v3.70.0 新增内容 - -- **OpenAI GPT-5.6 系列** — `Sol`、`Terra` 和 `Luna` 现已在 OpenAI Codex 和 OpenAI Native 两条提供商路径上可用。 -- **Grok 4.5 支持** — xAI 的全新旗舰模型,以及一项同样惠及 Grok 4 Mini 的 reasoning-effort 格式修复。 -- **Kenari 提供商支持** — 一个一流的、兼容 OpenAI 的 AI 网关,以卢比计费,覆盖 Claude、GPT、DeepSeek、GLM、Kimi 等更多模型。 -- 在折叠的任务标题中显示上下文压缩按钮和上下文窗口进度条。 -- 修复:在冷终端上出现的终端输出丢失和任务过早完成的问题。 -- 修复:Zoo Gateway 和 Vercel AI Gateway 模型的图片附加功能现在会跟随实时的 vision 能力标签,而不是静态的白名单。 -- 依赖和工具更新。 +## v3.72.0 新增内容 + +- **Moonshot 和 Kimi Code 提供商** — 动态发现 Moonshot 模型,或使用 OAuth 设备流程登录 Kimi Code。 +- **最新模型支持** — 跨提供商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 和 MiniMax-M3。 +- **改进的子任务工作流** — 干净地放弃中断的子任务,并通过更安全的任务历史失效机制和更完善的恢复测试提升可靠性。 +- **扩展代码库索引** — 为 Dart 和纯文本文件建立索引。 +- **提供商可靠性** — 修复 ChatGPT OAuth 下的 GPT-5.6 Luna、LiteLLM reasoning content 和 DeepSeek V4 图片输入问题。 +- **更顺畅的设置体验** — 改进 Zoo Gateway 登录和模式切换行为。 +- API、提供商类型、依赖、本地化和 CI 改进。 ## Zoo Code 能为您做什么? diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 4bced1aa72..cddf23cccb 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -32,15 +32,15 @@ 你可以在 [Roo→Zoo 遷移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到從 Roo Code 遷移到 Zoo Code 的快速說明。我們希望在大家轉移過程中盡可能提供協助,這也是我們設立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社群的原因。如果你遇到問題或有任何疑問,歡迎加入後直接提問。 -## v3.70.0 新功能 - -- **OpenAI GPT-5.6 系列** — `Sol`、`Terra` 和 `Luna` 現已在 OpenAI Codex 與 OpenAI Native 兩條供應商路徑上提供。 -- **Grok 4.5 支援** — xAI 全新的旗艦模型,以及一項同樣使 Grok 4 Mini 受惠的 reasoning-effort 格式修正。 -- **Kenari 供應商支援** — 一個一流的、相容 OpenAI 的 AI 閘道,以印尼盾計費,涵蓋 Claude、GPT、DeepSeek、GLM、Kimi 等更多模型。 -- 在摺疊的任務標頭中顯示內容壓縮按鈕與內容視窗進度條。 -- 修正:冷終端機上發生的終端機輸出遺失與任務過早完成問題。 -- 修正:Zoo Gateway 與 Vercel AI Gateway 模型的圖片附加功能現在會依循即時的 vision 能力標籤,而非靜態的白名單。 -- 相依套件與工具更新。 +## v3.72.0 新功能 + +- **Moonshot 與 Kimi Code 供應商** — 動態探索 Moonshot 模型,或使用 OAuth 裝置流程登入 Kimi Code。 +- **最新模型支援** — 跨供應商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 與 MiniMax-M3。 +- **改善的子任務工作流程** — 乾淨地放棄中斷的子任務,並透過更安全的任務歷史失效機制與更完整的恢復測試提升可靠性。 +- **擴充程式碼庫索引** — 為 Dart 與純文字檔案建立索引。 +- **供應商可靠性** — 修正 ChatGPT OAuth 下的 GPT-5.6 Luna、LiteLLM reasoning content 與 DeepSeek V4 圖片輸入問題。 +- **更順暢的設定體驗** — 改善 Zoo Gateway 登入與模式切換行為。 +- API、供應商類型、相依套件、本地化與 CI 改善。 ## Zoo Code 能為您做什麼? diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 68a440a6f5..fcfd916d8e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -205,7 +205,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jul-2026-v3.70.0-gpt5.6-grok4.5-kenari" // v3.70.0 GPT-5.6 family, Grok 4.5 support, Kenari provider + public readonly latestAnnouncementId = "jul-2026-v3.72.0-moonshot-kimi-models-workflows" // v3.72.0 Moonshot/Kimi providers, new models, subtask/indexing improvements public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager diff --git a/src/package.json b/src/package.json index 18b415580c..eea27ba3f9 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "ZooCodeOrganization", - "version": "3.70.0", + "version": "3.72.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index ee39c18478..f17bd96a03 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -13,7 +13,7 @@ vi.mock("@src/utils/vscode", () => ({ vi.mock("@roo/package", () => ({ Package: { - version: "3.55.0", + version: "3.72.0", }, })) @@ -41,11 +41,11 @@ vi.mock("@src/i18n/TranslationContext", () => ({ const translations: Record = { "chat:announcement.release.heading": "What's New:", "chat:announcement.release.highlight1": - "Xiaomi MiMo provider: Added Xiaomi MiMo as a first-class API provider so you can configure MiMo models directly in Zoo Code.", + "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", "chat:announcement.release.highlight2": - "Upstream Zoo Code handoff: Pulled in the latest upstream sunset merge and related platform updates to keep Zoo Code aligned with the community handoff work.", + "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", "chat:announcement.release.highlight3": - "Stability fixes across chat and providers: Fixed MCP sign-in copy, Gemini full-tool requests, OpenAI temperature handling, and Markdown single-tilde rendering.", + "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase.", "chat:announcement.handoff.heading": "The Roo Code plugin is not going away.", } @@ -62,20 +62,20 @@ describe("Announcement", () => { it("renders the announcement title and highlights", () => { render() - expect(screen.getByText("Zoo Code 3.55.0 Released")).toBeInTheDocument() + expect(screen.getByText("Zoo Code 3.72.0 Released")).toBeInTheDocument() expect( screen.getByText( - "Xiaomi MiMo provider: Added Xiaomi MiMo as a first-class API provider so you can configure MiMo models directly in Zoo Code.", + "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", ), ).toBeInTheDocument() expect( screen.getByText( - "Upstream Zoo Code handoff: Pulled in the latest upstream sunset merge and related platform updates to keep Zoo Code aligned with the community handoff work.", + "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", ), ).toBeInTheDocument() expect( screen.getByText( - "Stability fixes across chat and providers: Fixed MCP sign-in copy, Gemini full-tool requests, OpenAI temperature handling, and Markdown single-tilde rendering.", + "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase.", ), ).toBeInTheDocument() }) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index fe301eb4cc..d4e5e2524e 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -343,9 +343,9 @@ "support": "Dona suport a Zoo Code fent-nos una estrella a GitHub.", "release": { "heading": "Novetats:", - "highlight1": "Família OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible tant a OpenAI Codex com a OpenAI Native.", - "highlight2": "Suport per a Grok 4.5 — el nou model insígnia de xAI, més una correcció del reasoning effort que també beneficia Grok 4 Mini.", - "highlight3": "Proveïdor Kenari — un gateway d'IA de primera classe compatible amb OpenAI, facturat en rupies, que cobreix Claude, GPT, DeepSeek, GLM, Kimi i més." + "highlight1": "Proveïdors Moonshot i Kimi Code — connecta't als models de Moonshot amb detecció de models en temps real o inicia la sessió a Kimi Code mitjançant el flux de dispositiu OAuth.", + "highlight2": "Compatibilitat amb els models més recents — utilitza Claude Opus 5 amb tots els proveïdors, a més de Kimi K3, Gemini 3.6 Flash i MiniMax-M3.", + "highlight3": "Fluxos de treball més fiables — abandona subtasques interrompudes de manera neta i indexa fitxers Dart i de text pla a la teva base de codi." }, "cloudAgents": { "heading": "Novetats al núvol:", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index a2cc52b716..fb63e5b6a1 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -343,9 +343,9 @@ "support": "Bitte unterstütze Zoo Code, indem du uns auf GitHub einen Stern gibst.", "release": { "heading": "Was ist neu:", - "highlight1": "OpenAI-GPT-5.6-Familie (Sol, Terra, Luna) — verfügbar über OpenAI Codex und OpenAI Native.", - "highlight2": "Grok-4.5-Unterstützung — xAIs neues Flaggschiff-Modell, plus ein Fix für den Reasoning Effort, von dem auch Grok 4 Mini profitiert.", - "highlight3": "Kenari-Anbieter — ein erstklassiges OpenAI-kompatibles KI-Gateway, das in Rupiah abgerechnet wird und Claude, GPT, DeepSeek, GLM, Kimi und mehr abdeckt." + "highlight1": "Moonshot- und Kimi-Code-Anbieter — verbinde dich dank Live-Modellerkennung mit Moonshot-Modellen oder melde dich über den OAuth-Gerätefluss bei Kimi Code an.", + "highlight2": "Unterstützung für die neuesten Modelle — nutze Claude Opus 5 anbieterübergreifend sowie Kimi K3, Gemini 3.6 Flash und MiniMax-M3.", + "highlight3": "Zuverlässigere Workflows — brich unterbrochene Unteraufgaben sauber ab und indiziere Dart- und Klartextdateien in deiner Codebasis." }, "cloudAgents": { "heading": "Neu in der Cloud:", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 00438c3bfc..8df0c2938a 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -362,9 +362,9 @@ "support": "Please support Zoo Code by starring us on GitHub.", "release": { "heading": "What's New:", - "highlight1": "OpenAI GPT-5.6 family (Sol, Terra, Luna) — available across both the OpenAI Codex and OpenAI Native provider paths.", - "highlight2": "Grok 4.5 support — xAI's new flagship model, plus a reasoning-effort fix that also benefits Grok 4 Mini.", - "highlight3": "Kenari provider — a first-class OpenAI-compatible AI gateway billed in Rupiah, covering Claude, GPT, DeepSeek, GLM, Kimi and more." + "highlight1": "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", + "highlight2": "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", + "highlight3": "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase." }, "cloudAgents": { "heading": "New in the Cloud:", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index d690b09cf2..1a297de709 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -343,9 +343,9 @@ "support": "Apoya a Zoo Code dándonos una estrella en GitHub.", "release": { "heading": "Novedades:", - "highlight1": "Familia OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible tanto en OpenAI Codex como en OpenAI Native.", - "highlight2": "Compatibilidad con Grok 4.5 — el nuevo modelo insignia de xAI, además de una corrección del reasoning effort que también beneficia a Grok 4 Mini.", - "highlight3": "Proveedor Kenari — un gateway de IA de primera línea compatible con OpenAI, facturado en rupias, que cubre Claude, GPT, DeepSeek, GLM, Kimi y más." + "highlight1": "Proveedores Moonshot y Kimi Code — conéctate a modelos de Moonshot con detección de modelos en tiempo real o inicia sesión en Kimi Code mediante su flujo de dispositivo OAuth.", + "highlight2": "Compatibilidad con los modelos más recientes — usa Claude Opus 5 con todos los proveedores, además de Kimi K3, Gemini 3.6 Flash y MiniMax-M3.", + "highlight3": "Flujos de trabajo más fiables — abandona subtareas interrumpidas de forma limpia e indexa archivos Dart y de texto sin formato en tu base de código." }, "cloudAgents": { "heading": "Novedades en la Nube:", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 12daca0d26..24d27418e7 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -343,9 +343,9 @@ "support": "Soutiens Zoo Code en nous donnant une étoile sur GitHub.", "release": { "heading": "Nouveautés :", - "highlight1": "Famille OpenAI GPT-5.6 (Sol, Terra, Luna) — disponible à la fois sur OpenAI Codex et OpenAI Native.", - "highlight2": "Prise en charge de Grok 4.5 — le nouveau modèle phare de xAI, avec une correction du reasoning effort qui profite aussi à Grok 4 Mini.", - "highlight3": "Provider Kenari — un gateway IA de premier ordre compatible OpenAI, facturé en roupies, couvrant Claude, GPT, DeepSeek, GLM, Kimi et plus." + "highlight1": "Providers Moonshot et Kimi Code — connecte-toi aux modèles Moonshot avec la découverte en temps réel, ou à Kimi Code via son flux d'appareil OAuth.", + "highlight2": "Prise en charge des derniers modèles — utilise Claude Opus 5 avec tous les providers, ainsi que Kimi K3, Gemini 3.6 Flash et MiniMax-M3.", + "highlight3": "Workflows plus fiables — abandonne proprement les sous-tâches interrompues et indexe les fichiers Dart et texte brut de ta base de code." }, "cloudAgents": { "heading": "Nouveautés dans le Cloud :", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 167802f730..65306b39e8 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -343,9 +343,9 @@ "support": "कृपया GitHub पर हमें स्टार देकर Zoo Code का समर्थन करें।", "release": { "heading": "नया क्या है:", - "highlight1": "OpenAI GPT-5.6 परिवार (Sol, Terra, Luna) — OpenAI Codex और OpenAI Native दोनों में उपलब्ध।", - "highlight2": "Grok 4.5 समर्थन — xAI का नया flagship model, साथ ही reasoning effort की एक फिक्स जिसका फायदा Grok 4 Mini को भी मिलता है।", - "highlight3": "Kenari provider — एक first-class OpenAI-compatible AI gateway जो Rupiah में बिल होता है और Claude, GPT, DeepSeek, GLM, Kimi और अधिक को cover करता है।" + "highlight1": "Moonshot और Kimi Code providers — live model discovery के साथ Moonshot models से कनेक्ट करें या OAuth device flow से Kimi Code में sign in करें।", + "highlight2": "नवीनतम model support — providers पर Claude Opus 5 के साथ Kimi K3, Gemini 3.6 Flash और MiniMax-M3 का उपयोग करें।", + "highlight3": "अधिक भरोसेमंद workflows — interrupted subtasks को साफ़ तौर पर छोड़ें और अपनी codebase में Dart व plain-text files को index करें।" }, "cloudAgents": { "heading": "क्लाउड में नया:", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index c6f38c7d0e..fb11881cb4 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -372,9 +372,9 @@ "support": "Dukung Zoo Code dengan memberi kami bintang di GitHub.", "release": { "heading": "Yang Baru:", - "highlight1": "Keluarga OpenAI GPT-5.6 (Sol, Terra, Luna) — tersedia di OpenAI Codex maupun OpenAI Native.", - "highlight2": "Dukungan Grok 4.5 — model andalan baru xAI, plus perbaikan reasoning effort yang juga menguntungkan Grok 4 Mini.", - "highlight3": "Provider Kenari — gateway AI kelas utama yang kompatibel dengan OpenAI, ditagih dalam Rupiah, mencakup Claude, GPT, DeepSeek, GLM, Kimi, dan lainnya." + "highlight1": "Provider Moonshot dan Kimi Code — hubungkan ke model Moonshot dengan penemuan model secara langsung, atau masuk ke Kimi Code melalui alur perangkat OAuth.", + "highlight2": "Dukungan model terbaru — gunakan Claude Opus 5 di berbagai provider, ditambah Kimi K3, Gemini 3.6 Flash, dan MiniMax-M3.", + "highlight3": "Workflow lebih andal — tinggalkan subtask yang terinterupsi dengan bersih, serta indeks file Dart dan teks biasa di codebase-mu." }, "cloudAgents": { "heading": "Baru di Cloud:", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 79d61965c7..d6917d1be0 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -343,9 +343,9 @@ "support": "Sostieni Zoo Code lasciandoci una stella su GitHub.", "release": { "heading": "Novità:", - "highlight1": "Famiglia OpenAI GPT-5.6 (Sol, Terra, Luna) — disponibile sia su OpenAI Codex sia su OpenAI Native.", - "highlight2": "Supporto per Grok 4.5 — il nuovo modello di punta di xAI, più una correzione del reasoning effort che avvantaggia anche Grok 4 Mini.", - "highlight3": "Provider Kenari — un gateway AI di prima classe compatibile con OpenAI, fatturato in rupie, che copre Claude, GPT, DeepSeek, GLM, Kimi e altro." + "highlight1": "Provider Moonshot e Kimi Code — connettiti ai modelli Moonshot con il rilevamento dei modelli in tempo reale oppure accedi a Kimi Code tramite il flusso dispositivo OAuth.", + "highlight2": "Supporto per i modelli più recenti — usa Claude Opus 5 con tutti i provider, oltre a Kimi K3, Gemini 3.6 Flash e MiniMax-M3.", + "highlight3": "Workflow più affidabili — abbandona in modo pulito le sottoattività interrotte e indicizza i file Dart e di testo semplice nella tua codebase." }, "cloudAgents": { "heading": "Novità nel Cloud:", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 04e7b13691..17665a3754 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -343,9 +343,9 @@ "support": "GitHubでスターを付けて Zoo Code を応援してください。", "release": { "heading": "新機能:", - "highlight1": "OpenAI GPT-5.6 ファミリー(Sol、Terra、Luna)— OpenAI Codex と OpenAI Native の両方で利用可能。", - "highlight2": "Grok 4.5 サポート — xAI の新しいフラッグシップモデル。あわせて reasoning effort の修正も行われ、Grok 4 Mini にも恩恵があります。", - "highlight3": "Kenari プロバイダー — ルピー建てで請求される、Claude・GPT・DeepSeek・GLM・Kimi などに対応した OpenAI 互換のファーストクラス AI ゲートウェイ。" + "highlight1": "Moonshot と Kimi Code プロバイダー — ライブモデル検出で Moonshot モデルに接続するか、OAuth デバイスフローで Kimi Code にサインインできます。", + "highlight2": "最新モデルのサポート — 各プロバイダーで Claude Opus 5 を利用できるほか、Kimi K3、Gemini 3.6 Flash、MiniMax-M3 にも対応しました。", + "highlight3": "より信頼性の高いワークフロー — 中断されたサブタスクを安全に破棄し、コードベース内の Dart ファイルとプレーンテキストファイルをインデックスできます。" }, "cloudAgents": { "heading": "クラウドの新機能:", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 5b054bc9ce..53186e7afe 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -343,9 +343,9 @@ "support": "GitHub에서 별표를 눌러 Zoo Code를 응원해 주세요.", "release": { "heading": "새로운 기능:", - "highlight1": "OpenAI GPT-5.6 패밀리(Sol, Terra, Luna) — OpenAI Codex와 OpenAI Native 양쪽에서 사용 가능합니다.", - "highlight2": "Grok 4.5 지원 — xAI의 새로운 플래그십 모델이며, Grok 4 Mini에도 도움이 되는 reasoning effort 수정도 포함됩니다.", - "highlight3": "Kenari 프로바이더 — 루피아로 청구되며 Claude, GPT, DeepSeek, GLM, Kimi 등을 지원하는 OpenAI 호환 퍼스트클래스 AI 게이트웨이." + "highlight1": "Moonshot 및 Kimi Code 프로바이더 — 실시간 모델 검색으로 Moonshot 모델에 연결하거나 OAuth 기기 흐름으로 Kimi Code에 로그인하세요.", + "highlight2": "최신 모델 지원 — 여러 프로바이더에서 Claude Opus 5를 사용하고 Kimi K3, Gemini 3.6 Flash, MiniMax-M3도 이용하세요.", + "highlight3": "더 안정적인 워크플로우 — 중단된 하위 작업을 깔끔하게 포기하고 코드베이스의 Dart 및 일반 텍스트 파일을 인덱싱하세요." }, "cloudAgents": { "heading": "클라우드의 새로운 기능:", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index e8fdff65de..356b6f618b 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -316,9 +316,9 @@ "support": "Steun Zoo Code door ons een ster te geven op GitHub.", "release": { "heading": "Wat is nieuw:", - "highlight1": "OpenAI GPT-5.6-familie (Sol, Terra, Luna) — beschikbaar via zowel OpenAI Codex als OpenAI Native.", - "highlight2": "Ondersteuning voor Grok 4.5 — xAI's nieuwe vlaggenschipmodel, plus een fix voor reasoning effort waar ook Grok 4 Mini baat bij heeft.", - "highlight3": "Kenari-provider — een eersteklas OpenAI-compatibele AI-gateway die in Rupiah wordt gefactureerd en Claude, GPT, DeepSeek, GLM, Kimi en meer ondersteunt." + "highlight1": "Moonshot- en Kimi Code-providers — maak verbinding met Moonshot-modellen via live modeldetectie of meld je aan bij Kimi Code via de OAuth-apparaatstroom.", + "highlight2": "Ondersteuning voor de nieuwste modellen — gebruik Claude Opus 5 bij verschillende providers, plus Kimi K3, Gemini 3.6 Flash en MiniMax-M3.", + "highlight3": "Betrouwbaardere workflows — breek onderbroken subtaken netjes af en indexeer Dart- en plattetekstbestanden in je codebase." }, "cloudAgents": { "heading": "Nieuw in de Cloud:", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index a7ba2dc26a..9cf15a939d 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -343,9 +343,9 @@ "support": "Wesprzyj Zoo Code, dając nam gwiazdkę na GitHub.", "release": { "heading": "Co nowego:", - "highlight1": "Rodzina OpenAI GPT-5.6 (Sol, Terra, Luna) — dostępna zarówno w OpenAI Codex, jak i OpenAI Native.", - "highlight2": "Obsługa Grok 4.5 — nowy flagowy model xAI, a także poprawka reasoning effort, z której korzysta również Grok 4 Mini.", - "highlight3": "Provider Kenari — wysokiej klasy gateway AI kompatybilny z OpenAI, rozliczany w rupii, obsługujący Claude, GPT, DeepSeek, GLM, Kimi i więcej." + "highlight1": "Providerzy Moonshot i Kimi Code — łącz się z modelami Moonshot dzięki wykrywaniu modeli na żywo lub zaloguj się do Kimi Code przez przepływ urządzenia OAuth.", + "highlight2": "Obsługa najnowszych modeli — korzystaj z Claude Opus 5 u różnych providerów, a także z Kimi K3, Gemini 3.6 Flash i MiniMax-M3.", + "highlight3": "Bardziej niezawodne workflow — bezpiecznie porzucaj przerwane podzadania i indeksuj pliki Dart oraz pliki tekstowe w swojej bazie kodu." }, "cloudAgents": { "heading": "Nowości w chmurze:", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index b70d5804f5..771d21cd86 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -343,9 +343,9 @@ "support": "Apoie o Zoo Code nos dando uma estrela no GitHub.", "release": { "heading": "Novidades:", - "highlight1": "Família OpenAI GPT-5.6 (Sol, Terra, Luna) — disponível tanto no OpenAI Codex quanto no OpenAI Native.", - "highlight2": "Suporte ao Grok 4.5 — o novo modelo principal da xAI, além de uma correção no reasoning effort que também beneficia o Grok 4 Mini.", - "highlight3": "Provider Kenari — um gateway de IA de primeira classe compatível com OpenAI, cobrado em rúpias, cobrindo Claude, GPT, DeepSeek, GLM, Kimi e mais." + "highlight1": "Providers Moonshot e Kimi Code — conecte-se aos modelos Moonshot com descoberta de modelos em tempo real ou entre no Kimi Code pelo fluxo de dispositivo OAuth.", + "highlight2": "Suporte aos modelos mais recentes — use Claude Opus 5 em vários providers, além de Kimi K3, Gemini 3.6 Flash e MiniMax-M3.", + "highlight3": "Workflows mais confiáveis — abandone subtarefas interrompidas de forma limpa e indexe arquivos Dart e de texto simples na sua base de código." }, "cloudAgents": { "heading": "Novidades na Nuvem:", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 7b8a57d722..0e1f45ff17 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -317,9 +317,9 @@ "support": "Поддержите Zoo Code, поставив нам звезду на GitHub.", "release": { "heading": "Что нового:", - "highlight1": "Семейство OpenAI GPT-5.6 (Sol, Terra, Luna) — доступно как в OpenAI Codex, так и в OpenAI Native.", - "highlight2": "Поддержка Grok 4.5 — новая флагманская модель xAI, а также исправление reasoning effort, от которого выигрывает и Grok 4 Mini.", - "highlight3": "Провайдер Kenari — первоклассный AI-шлюз, совместимый с OpenAI, с выставлением счётов в рупиях, поддерживающий Claude, GPT, DeepSeek, GLM, Kimi и другие модели." + "highlight1": "Провайдеры Moonshot и Kimi Code — подключайтесь к моделям Moonshot с динамическим обнаружением моделей или входите в Kimi Code через OAuth-поток для устройств.", + "highlight2": "Поддержка новейших моделей — используйте Claude Opus 5 у разных провайдеров, а также Kimi K3, Gemini 3.6 Flash и MiniMax-M3.", + "highlight3": "Более надёжные рабочие процессы — безопасно завершайте прерванные подзадачи и индексируйте файлы Dart и обычные текстовые файлы в кодовой базе." }, "cloudAgents": { "heading": "Новое в облаке:", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 8aaa9e58e1..2e9b107008 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -344,9 +344,9 @@ "support": "Lütfen GitHub'da bize yıldız vererek Zoo Code'u destekle.", "release": { "heading": "Yenilikler:", - "highlight1": "OpenAI GPT-5.6 ailesi (Sol, Terra, Luna) — hem OpenAI Codex hem de OpenAI Native üzerinde kullanılabilir.", - "highlight2": "Grok 4.5 desteği — xAI'nin yeni amiral gemisi modeli, ayrıca Grok 4 Mini'ye de fayda sağlayan bir reasoning effort düzeltmesi.", - "highlight3": "Kenari provider — Rupiah üzerinden faturalandırılan, Claude, GPT, DeepSeek, GLM, Kimi ve daha fazlasını kapsayan, OpenAI uyumlu birinci sınıf bir AI ağ geçidi." + "highlight1": "Moonshot ve Kimi Code provider'ları — canlı model keşfiyle Moonshot modellerine bağlan veya OAuth cihaz akışıyla Kimi Code'da oturum aç.", + "highlight2": "En yeni model desteği — provider'lar genelinde Claude Opus 5'in yanı sıra Kimi K3, Gemini 3.6 Flash ve MiniMax-M3'ü kullan.", + "highlight3": "Daha güvenilir iş akışları — kesintiye uğramış alt görevleri temiz biçimde bırak ve kod tabanındaki Dart ile düz metin dosyalarını indeksle." }, "cloudAgents": { "heading": "Cloud'daki yenilikler:", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 54d46edf6a..7737ea7b75 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -344,9 +344,9 @@ "support": "Hãy ủng hộ Zoo Code bằng cách tặng sao cho chúng tôi trên GitHub.", "release": { "heading": "Có gì mới:", - "highlight1": "Dòng OpenAI GPT-5.6 (Sol, Terra, Luna) — có sẵn trên cả OpenAI Codex và OpenAI Native.", - "highlight2": "Hỗ trợ Grok 4.5 — mẫu chủ lực mới của xAI, cùng với bản sửa lỗi reasoning effort cũng có lợi cho Grok 4 Mini.", - "highlight3": "Provider Kenari — gateway AI hạng nhất tương thích OpenAI, tính phí bằng Rupiah, hỗ trợ Claude, GPT, DeepSeek, GLM, Kimi và nhiều hơn nữa." + "highlight1": "Provider Moonshot và Kimi Code — kết nối với các model Moonshot bằng tính năng khám phá model trực tiếp hoặc đăng nhập Kimi Code qua luồng thiết bị OAuth.", + "highlight2": "Hỗ trợ model mới nhất — dùng Claude Opus 5 trên nhiều provider, cùng với Kimi K3, Gemini 3.6 Flash và MiniMax-M3.", + "highlight3": "Workflow đáng tin cậy hơn — từ bỏ gọn gàng các tác vụ con bị gián đoạn và lập chỉ mục file Dart cùng file văn bản thuần trong codebase." }, "cloudAgents": { "heading": "Mới trên Cloud:", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index c3621c4009..3322c53542 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -344,9 +344,9 @@ "support": "请在 GitHub 上为 Zoo Code 点星支持我们。", "release": { "heading": "新增功能:", - "highlight1": "OpenAI GPT-5.6 系列(Sol、Terra、Luna)— 已在 OpenAI Codex 与 OpenAI Native 中同时提供。", - "highlight2": "支持 Grok 4.5 — xAI 全新旗舰模型,同时修复了 reasoning effort 问题,Grok 4 Mini 也因此受益。", - "highlight3": "Kenari 提供商 — 以卢比计费的一流 OpenAI 兼容 AI 网关,支持 Claude、GPT、DeepSeek、GLM、Kimi 等。" + "highlight1": "Moonshot 和 Kimi Code 提供商 — 通过实时模型发现连接 Moonshot 模型,或使用 OAuth 设备流程登录 Kimi Code。", + "highlight2": "最新模型支持 — 跨提供商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 和 MiniMax-M3。", + "highlight3": "更可靠的工作流 — 干净地放弃中断的子任务,并为代码库中的 Dart 和纯文本文件建立索引。" }, "cloudAgents": { "heading": "云端新功能:", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index cf7439cd13..3807cd2489 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -365,9 +365,9 @@ "support": "請在 GitHub 上為 Zoo Code 加星支持我們。", "release": { "heading": "新增功能:", - "highlight1": "OpenAI GPT-5.6 系列(Sol、Terra、Luna)— 已在 OpenAI Codex 與 OpenAI Native 中同時提供。", - "highlight2": "支援 Grok 4.5 — xAI 全新旗艦模型,同時修正了 reasoning effort 問題,Grok 4 Mini 也因此受惠。", - "highlight3": "Kenari 供應商 — 以盧比計費的一流 OpenAI 相容 AI 閘道,支援 Claude、GPT、DeepSeek、GLM、Kimi 等。" + "highlight1": "Moonshot 與 Kimi Code 供應商 — 透過即時模型探索連接 Moonshot 模型,或使用 OAuth 裝置流程登入 Kimi Code。", + "highlight2": "最新模型支援 — 跨供應商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 與 MiniMax-M3。", + "highlight3": "更可靠的工作流程 — 乾淨地放棄中斷的子任務,並為程式碼庫中的 Dart 與純文字檔案建立索引。" }, "cloudAgents": { "heading": "雲端的新功能:", From 116acfdb6c8cce6a45b80526e212e736cefd2f0c Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:55:09 -0400 Subject: [PATCH 56/89] [Fix] Flaky mocked e2e subtasks test when parent resume is served the child fixture (#1002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(e2e-mock): fix subtasks fixture collision between child and parent-resume fixtures (#1001) The fast-child and SUBTASK_CHILD fixtures matched a bare marker that the parent prompt embeds verbatim, so parent-resume turns could be served the child fixture (mirrors #561, which fixed the same class for cross-profile fixtures only). Parent-resume fixtures also guarded on the literal new_task tool-call id, which validateAndFixToolResultIds can rewrite on resume. - Add parent-marker exclusion (with aimock's last-user-message scoping) to the fast-child and SUBTASK_CHILD fixtures - Guard parent-resume fixtures on the injected subtask result content instead of the fragile tool-call id * test(e2e-mock): address PR review — injection-guard all parent fixtures, link fixture format contract - Regular parent turn-1 fixture: exclude resume turns via SUBTASK_RESULT_INJECTION instead of relying on registration order (sequenceIndex is not viable here — the fixture is shared by 4 tests) - Fast and api-hang parent-resume guards: match SUBTASK_RESULT_INJECTION instead of the child result text, which is embedded verbatim in the parent prompts and could match the parent's initial request on retry - Add unit-test assertion pinning reopenParentFromDelegation's injected tool_result format to the 'completed.\n\nResult:' contract the e2e fixtures match on - lastUserMessageContains: mirror aimock's getTextContent (text parts only) instead of JSON.stringify for non-string content - Extract SUBTASK_FAST_CHILD_RESULT constant - Replace tautological find()/assert pairs in the api-hang test - Move the no-parent-resume assertion in the cancellation test behind the async settle gates and also assert no completion_result --------- Co-authored-by: Roomote --- apps/vscode-e2e/src/fixtures/subtasks.ts | 67 +++++++++++++++---- apps/vscode-e2e/src/suite/subtasks.test.ts | 24 +++++-- .../history-resume-delegation.spec.ts | 6 ++ 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index 5a39577dd9..890c243b38 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -18,7 +18,8 @@ const SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER = "SUBTASK_CHILD_DIFFERENT_PROFILE const SUBTASK_CHILD_PROMPT = `${SUBTASK_CHILD_MARKER}: Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.` export const SUBTASK_PARENT_PROMPT = `${SUBTASK_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_CHILD_PROMPT}" Do not answer directly.` export const SUBTASK_CHILD_FOLLOWUP_ANSWER = "9" -const SUBTASK_FAST_CHILD_PROMPT = `${SUBTASK_FAST_CHILD_MARKER}: Complete immediately with the exact result "Fast child completed".` +export const SUBTASK_FAST_CHILD_RESULT = "Fast child completed" +const SUBTASK_FAST_CHILD_PROMPT = `${SUBTASK_FAST_CHILD_MARKER}: Complete immediately with the exact result "${SUBTASK_FAST_CHILD_RESULT}".` export const SUBTASK_FAST_PARENT_PROMPT = `${SUBTASK_FAST_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_FAST_CHILD_PROMPT}" Do not answer directly.` const SUBTASK_INTERRUPT_CHILD_PROMPT = `${SUBTASK_INTERRUPT_CHILD_MARKER}: Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.` @@ -54,6 +55,31 @@ const requestContains = (req: ChatCompletionRequest, expected: string[]) => { return expected.every((text) => rawRequest.includes(text)) } +// aimock's `userMessage` matcher only inspects the LAST user message and joins only the +// `type: "text"` content parts (see getTextContent in aimock's router). Fixtures that need +// whole-request exclusions must replicate that scoping inside a predicate so they keep the +// same matching semantics as the bare-regex fixtures they replace. +const lastUserMessageContains = (req: ChatCompletionRequest, text: string) => { + const userMessages = req.messages?.filter((message) => message.role === "user") ?? [] + const last = userMessages.at(-1) + if (!last) return false + const content = + typeof last.content === "string" + ? last.content + : (last.content ?? []) + .filter((part): part is { type: "text"; text: string } => part?.type === "text") + .map((part) => part.text) + .join("") + return content.includes(text) +} + +// reopenParentFromDelegation injects the child result into the resumed parent's history as +// `Subtask completed.\n\nResult:\n`. Matching on this injected prefix (in +// its JSON-serialized form) keeps parent-resume fixtures robust when +// validateAndFixToolResultIds rewrites tool-use ids on resume — matching on the new_task +// tool-call id directly proved flaky (the id can be rewritten, the fixture then misses, and +// a looser child fixture wins and serves the child's response to the parent). +const SUBTASK_RESULT_INJECTION = "completed.\\n\\nResult:" const completionAfterAnswer = (followupId: string, completionId: string) => ({ match: { predicate: (req: ChatCompletionRequest) => @@ -100,25 +126,33 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + // The parent prompt embeds SUBTASK_FAST_CHILD_MARKER verbatim, so parent-resume turns + // can also match a bare substring check (same collision class as #561). Exclude the + // parent marker so those turns fall through to the parent-resume fixture below. mock.addFixture({ match: { - userMessage: new RegExp(SUBTASK_FAST_CHILD_MARKER), + predicate: (req: ChatCompletionRequest) => + lastUserMessageContains(req, SUBTASK_FAST_CHILD_MARKER) && + !requestContains(req, [SUBTASK_FAST_PARENT_MARKER]), }, response: { toolCalls: [ { name: "attempt_completion", - arguments: JSON.stringify({ result: "Fast child completed" }), + arguments: JSON.stringify({ result: SUBTASK_FAST_CHILD_RESULT }), id: "call_subtasks_fast_child_completion_002", }, ], }, }) + // Guard on SUBTASK_RESULT_INJECTION (not the child result text): the child result is + // embedded verbatim in SUBTASK_FAST_PARENT_PROMPT, so it cannot distinguish the parent's + // initial request from its resume turn. mock.addFixture({ match: { predicate: (req: ChatCompletionRequest) => - requestContains(req, [SUBTASK_FAST_PARENT_MARKER, "call_subtasks_fast_parent_new_task_001"]), + requestContains(req, [SUBTASK_FAST_PARENT_MARKER, SUBTASK_RESULT_INJECTION]), }, response: { toolCalls: [ @@ -131,9 +165,15 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + // This fixture is shared by several tests, so sequenceIndex cannot guard it. Exclude + // resume turns via the injected tool_result prefix instead: when the tool_result is + // serialized as role:"tool", the original parent prompt is the last user message again + // and would otherwise re-serve new_task on the parent's resume turn. mock.addFixture({ match: { - userMessage: new RegExp(SUBTASK_PARENT_MARKER), + predicate: (req: ChatCompletionRequest) => + lastUserMessageContains(req, SUBTASK_PARENT_MARKER) && + !requestContains(req, [SUBTASK_RESULT_INJECTION]), }, response: { toolCalls: [ @@ -149,9 +189,12 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + // Same collision guard as the fast-child fixture above: SUBTASK_PARENT_PROMPT embeds + // SUBTASK_CHILD_MARKER verbatim, so parent-resume turns must not match this fixture. mock.addFixture({ match: { - userMessage: new RegExp(SUBTASK_CHILD_MARKER), + predicate: (req: ChatCompletionRequest) => + lastUserMessageContains(req, SUBTASK_CHILD_MARKER) && !requestContains(req, [SUBTASK_PARENT_MARKER]), }, response: { toolCalls: [ @@ -172,7 +215,7 @@ export function addSubtaskFixtures(mock: InstanceType) { mock.addFixture({ match: { predicate: (req: ChatCompletionRequest) => - requestContains(req, [SUBTASK_PARENT_MARKER, "call_subtasks_parent_new_task_001"]), + requestContains(req, [SUBTASK_PARENT_MARKER, SUBTASK_RESULT_INJECTION]), }, response: { toolCalls: [ @@ -238,14 +281,12 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + // Same as the fast parent-resume fixture: the child result is embedded verbatim in + // SUBTASK_API_HANG_PARENT_PROMPT, so guard on the injected tool_result prefix instead. mock.addFixture({ match: { predicate: (req: ChatCompletionRequest) => - requestContains(req, [ - SUBTASK_API_HANG_PARENT_MARKER, - "call_api_hang_parent_new_task_001", - SUBTASK_API_HANG_CHILD_RESULT, - ]), + requestContains(req, [SUBTASK_API_HANG_PARENT_MARKER, SUBTASK_RESULT_INJECTION]), }, response: { toolCalls: [ @@ -433,7 +474,7 @@ export function addSubtaskFixtures(mock: InstanceType) { mock.addFixture({ match: { predicate: (req: ChatCompletionRequest) => - requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER, "call_interrupt_parent_new_task_001"]), + requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER, SUBTASK_RESULT_INJECTION]), }, response: { toolCalls: [ diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 661d7f0e2d..641e02f6ac 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -14,6 +14,7 @@ import { SUBTASK_API_HANG_PARENT_RESULT, SUBTASK_API_HANG_RESUME_MESSAGE, SUBTASK_CHILD_FOLLOWUP_ANSWER, + SUBTASK_FAST_CHILD_RESULT, SUBTASK_FAST_PARENT_PROMPT, SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER, SUBTASK_INTERRUPT_PARENT_PROMPT, @@ -101,7 +102,8 @@ suite("Roo Code Subtasks", function () { ([taskId, messages]) => taskId !== parentTaskId && messages.some( - ({ say, text }) => say === "completion_result" && text?.trim() === "Fast child completed", + ({ say, text }) => + say === "completion_result" && text?.trim() === SUBTASK_FAST_CHILD_RESULT, ), ), "Immediately-completing child should emit its expected result", @@ -357,15 +359,23 @@ suite("Roo Code Subtasks", function () { await api.cancelCurrentTask() + // Gate on the async settle before asserting the parent never resumed: a spurious + // resume would be an async downstream effect of the cancellation, so a synchronous + // check right after cancelCurrentTask() proves nothing. + await waitFor(() => api.getCurrentTaskStack().at(-1) === spawnedTaskId) + await waitFor( + () => asks[spawnedTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + ) + assert.ok( messages[parentTaskId]?.find(({ type, text }) => type === "say" && text === "Parent task resumed") === undefined, "Parent task should not have resumed after subtask cancellation", ) - - await waitFor(() => api.getCurrentTaskStack().at(-1) === spawnedTaskId) - await waitFor( - () => asks[spawnedTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + assert.strictEqual( + messages[parentTaskId]?.find(({ say }) => say === "completion_result"), + undefined, + "Parent must not have completed after subtask cancellation", ) await api.clearCurrentTask() @@ -539,7 +549,7 @@ suite("Roo Code Subtasks", function () { says[childTaskId!] ?.filter(({ say }) => say === "completion_result") .map(({ text }) => text?.trim()) - .find((text) => text === SUBTASK_API_HANG_CHILD_RESULT), + .find((text): text is string => !!text), SUBTASK_API_HANG_CHILD_RESULT, "Child should complete with its expected result after resume", ) @@ -547,7 +557,7 @@ suite("Roo Code Subtasks", function () { says[parentTaskId] ?.filter(({ say }) => say === "completion_result") .map(({ text }) => text?.trim()) - .find((text) => text === SUBTASK_API_HANG_PARENT_RESULT), + .find((text): text is string => !!text), SUBTASK_API_HANG_PARENT_RESULT, "Parent should resume and complete with its expected result", ) diff --git a/src/__tests__/history-resume-delegation.spec.ts b/src/__tests__/history-resume-delegation.spec.ts index 9c54ba3301..fc496d0c84 100644 --- a/src/__tests__/history-resume-delegation.spec.ts +++ b/src/__tests__/history-resume-delegation.spec.ts @@ -345,6 +345,12 @@ describe("History resume delegation - parent metadata transitions", () => { expect(injectedMsg.role).toBe("user") expect((injectedMsg.content[0] as any).type).toBe("tool_result") expect((injectedMsg.content[0] as any).tool_use_id).toBe("toolu_abc123") + + // Format contract with the e2e mock fixtures: the parent-resume fixtures in + // apps/vscode-e2e/src/fixtures/subtasks.ts match on this injected + // "completed.\n\nResult:" prefix (SUBTASK_RESULT_INJECTION). If this template + // changes, update the fixtures in the same PR or they silently never fire. + expect((injectedMsg.content[0] as any).content).toMatch(/^Subtask .+ completed\.\n\nResult:\n/) }) it("reopenParentFromDelegation injects plain text when no new_task tool_use exists in API history", async () => { From 933ee9843efd27800cbb786c5062e9c3329f9c9a Mon Sep 17 00:00:00 2001 From: JunyongParkDev Date: Sat, 25 Jul 2026 21:55:09 +0900 Subject: [PATCH 57/89] fix(settings): buffer Save-managed settings in cachedState until Save (#872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(settings): buffer Save-managed settings in cachedState until Save Several controls inside SettingsView wrote their changes to the extension host immediately via `updateSettings`, instead of buffering them in local `cachedState` and waiting for the user to click Save. Because Discard reverts edits by resetting `cachedState` to the persisted state, any control that already posted `updateSettings` poisoned that source of truth — so Discard could no longer undo the change, silently breaking the Save/Discard contract. Route all five Save-managed values exclusively through `cachedState` / `setCachedStateField` while SettingsView is open: - allowedCommands / deniedCommands (AutoApproveSettings): drop the immediate updateSettings on add/remove. - profileThresholds (ContextManagementSettings): drop the immediate updateSettings on the non-default profile branch; remove the now-unused vscode import. - includeTaskHistoryInEnhance (PromptsSettings): drop the immediate updateSettings on toggle. - mcpEnabled (McpEnabledToggle / McpView): adopt the existing "props first, fall back to context" pattern so SettingsView can pass the buffered value and a cachedState-backed setter; uncontrolled usage keeps the original immediate behavior. Genuinely-immediate actions (autoApprovalEnabled, per-server MCP toggle/delete/restart/timeout) are not in the Save payload and are left unchanged. The extension host side is correct and untouched. Add/update tests at the webview-ui component layer: buffering assertions for each control, a McpEnabledToggle controlled/uncontrolled regression, and a SettingsView Discard regression. Signed-off-by: JunyongParkDev * test(settings): cover buffered MCP enablement * test(settings): verify discarded commands are reverted in UI Strengthen the SettingsView regression test to verify that buffered commands disappear from the UI when changes are discarded. Signed-off-by: JunyongParkDev * fix(settings): restore immediate persistence for uncontrolled includeTaskHistoryInEnhance The previous Save-buffering change dropped the updateSettings post unconditionally, which fixed the controlled path but broke the uncontrolled one: with props omitted the toggle falls back to the context setter, which only updates local state and never persists. Gate the post on whether the component is controlled: - Controlled: setter buffers into cachedState, Save persists — no post here. - Uncontrolled: context setter is local-only, so post updateSettings now. Mirrors the props-first / context-fallback pattern used for McpEnabledToggle. Adds an uncontrolled regression test (updates local state *and* posts), symmetric with the controlled "buffer only" case. Signed-off-by: JunyongParkDev --------- Signed-off-by: JunyongParkDev --- .../src/components/mcp/McpEnabledToggle.tsx | 24 ++++- webview-ui/src/components/mcp/McpView.tsx | 15 ++- .../mcp/__tests__/McpEnabledToggle.spec.tsx | 58 ++++++++++ .../components/mcp/__tests__/McpView.spec.tsx | 77 ++++++++++++++ .../settings/AutoApproveSettings.tsx | 12 --- .../settings/ContextManagementSettings.tsx | 2 - .../components/settings/PromptsSettings.tsx | 14 ++- .../src/components/settings/SettingsView.tsx | 7 +- .../__tests__/AutoApproveSettings.spec.tsx | 100 ++++++++++++++++++ .../ContextManagementSettings.spec.tsx | 33 +++++- .../__tests__/PromptsSettings.spec.tsx | 91 ++++++++++++++++ .../settings/__tests__/SettingsView.spec.tsx | 70 +++++++++--- .../SettingsView.unsaved-changes.spec.tsx | 46 ++++++-- 13 files changed, 500 insertions(+), 49 deletions(-) create mode 100644 webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx create mode 100644 webview-ui/src/components/mcp/__tests__/McpView.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx diff --git a/webview-ui/src/components/mcp/McpEnabledToggle.tsx b/webview-ui/src/components/mcp/McpEnabledToggle.tsx index 1fd30034b5..3e6e5cf9ff 100644 --- a/webview-ui/src/components/mcp/McpEnabledToggle.tsx +++ b/webview-ui/src/components/mcp/McpEnabledToggle.tsx @@ -5,10 +5,22 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" -const McpEnabledToggle = () => { - const { mcpEnabled, setMcpEnabled } = useExtensionState() +interface McpEnabledToggleProps { + mcpEnabled?: boolean + setMcpEnabled?: (value: boolean) => void +} + +const McpEnabledToggle = ({ + mcpEnabled: propsMcpEnabled, + setMcpEnabled: propsSetMcpEnabled, +}: McpEnabledToggleProps = {}) => { + const { mcpEnabled: contextMcpEnabled, setMcpEnabled: contextSetMcpEnabled } = useExtensionState() const { t } = useAppTranslation() + // When rendered inside SettingsView the value is buffered in `cachedState` and + // only persisted on Save. Fall back to live extension state when used uncontrolled. + const mcpEnabled = propsMcpEnabled ?? contextMcpEnabled + const handleChange = (e: Event | FormEvent) => { const target = ("target" in e ? e.target : null) as HTMLInputElement | null @@ -16,8 +28,12 @@ const McpEnabledToggle = () => { return } - setMcpEnabled(target.checked) - vscode.postMessage({ type: "updateSettings", updatedSettings: { mcpEnabled: target.checked } }) + if (propsSetMcpEnabled) { + propsSetMcpEnabled(target.checked) + } else { + contextSetMcpEnabled(target.checked) + vscode.postMessage({ type: "updateSettings", updatedSettings: { mcpEnabled: target.checked } }) + } } return ( diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 75a9a1a380..72724d72e8 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -28,8 +28,17 @@ import McpResourceRow from "./McpResourceRow" import McpEnabledToggle from "./McpEnabledToggle" import { McpErrorRow } from "./McpErrorRow" -const McpView = () => { - const { mcpServers: servers, alwaysAllowMcp, mcpEnabled } = useExtensionState() +interface McpViewProps { + mcpEnabled?: boolean + setMcpEnabled?: (value: boolean) => void +} + +const McpView = ({ mcpEnabled: propsMcpEnabled, setMcpEnabled }: McpViewProps = {}) => { + const { mcpServers: servers, alwaysAllowMcp, mcpEnabled: contextMcpEnabled } = useExtensionState() + + // When rendered inside SettingsView the value is buffered in `cachedState` and + // only persisted on Save. Fall back to live extension state when used uncontrolled. + const mcpEnabled = propsMcpEnabled ?? contextMcpEnabled const { t } = useAppTranslation() const { isOverThreshold, title, message } = useTooManyTools() @@ -55,7 +64,7 @@ const McpView = () => {
- + {mcpEnabled && ( <> diff --git a/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx new file mode 100644 index 0000000000..f59efce971 --- /dev/null +++ b/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx @@ -0,0 +1,58 @@ +// npx vitest src/components/mcp/__tests__/McpEnabledToggle.spec.tsx + +import { render, screen, fireEvent } from "@/utils/test-utils" + +import McpEnabledToggle from "../McpEnabledToggle" +import { vscode } from "@src/utils/vscode" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +const contextSetMcpEnabled = vi.fn() +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + mcpEnabled: true, + setMcpEnabled: contextSetMcpEnabled, + }), +})) + +const getCheckbox = () => screen.getByRole("checkbox") + +describe("McpEnabledToggle - Save/Discard contract", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Case 6: controlled (inside SettingsView) must buffer, not persist before Save. + it("buffers via the setter prop without persisting before Save when controlled", () => { + const setMcpEnabled = vi.fn() + render() + + fireEvent.click(getCheckbox()) + + expect(setMcpEnabled).toHaveBeenCalledWith(false) + expect(vscode.postMessage).not.toHaveBeenCalled() + // Must not touch live extension state either. + expect(contextSetMcpEnabled).not.toHaveBeenCalled() + }) + + // Regression guard: uncontrolled usage keeps the original immediate behavior. + it("persists immediately via live state when used uncontrolled", () => { + render() + + fireEvent.click(getCheckbox()) + + expect(contextSetMcpEnabled).toHaveBeenCalledWith(false) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { mcpEnabled: false }, + }) + }) +}) diff --git a/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx new file mode 100644 index 0000000000..957797d1e9 --- /dev/null +++ b/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen } from "@/utils/test-utils" + +import McpView from "../McpView" + +const setMcpEnabled = vi.fn() + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + mcpServers: [], + alwaysAllowMcp: false, + mcpEnabled: false, + }), +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@src/hooks/useTooManyTools", () => ({ + useTooManyTools: () => ({ isOverThreshold: false, title: "", message: "" }), +})) + +vi.mock("@src/components/settings/Section", () => ({ + Section: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock("@src/components/settings/SectionHeader", () => ({ + SectionHeader: ({ children }: { children: React.ReactNode }) =>

{children}

, +})) + +vi.mock("@src/components/ui", () => ({ + Button: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( + + ), + Dialog: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogDescription: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogFooter: ({ children }: { children: React.ReactNode }) =>
{children}
, + ToggleSwitch: () => null, + StandardTooltip: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +vi.mock("../McpEnabledToggle", () => ({ + default: ({ + mcpEnabled, + setMcpEnabled: setEnabled, + }: { + mcpEnabled: boolean + setMcpEnabled?: (value: boolean) => void + }) => ( + + ), +})) + +describe("McpView", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("uses extension state when no buffered value is provided", () => { + render() + + expect(screen.getByTestId("mcp-enabled-toggle")).toHaveTextContent("false") + }) + + it("uses and updates the buffered value when controlled", () => { + render() + + fireEvent.click(screen.getByTestId("mcp-enabled-toggle")) + + expect(setMcpEnabled).toHaveBeenCalledWith(false) + }) +}) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 40e1658f5f..90b82dc4be 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -88,7 +88,6 @@ export const AutoApproveSettings = ({ const newCommands = [...currentCommands, commandInput] setCachedStateField("allowedCommands", newCommands) setCommandInput("") - vscode.postMessage({ type: "updateSettings", updatedSettings: { allowedCommands: newCommands } }) } } @@ -99,7 +98,6 @@ export const AutoApproveSettings = ({ const newCommands = [...currentCommands, deniedCommandInput] setCachedStateField("deniedCommands", newCommands) setDeniedCommandInput("") - vscode.postMessage({ type: "updateSettings", updatedSettings: { deniedCommands: newCommands } }) } } @@ -317,11 +315,6 @@ export const AutoApproveSettings = ({ onClick={() => { const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index) setCachedStateField("allowedCommands", newCommands) - - vscode.postMessage({ - type: "updateSettings", - updatedSettings: { allowedCommands: newCommands }, - }) }}>
{cmd}
@@ -376,11 +369,6 @@ export const AutoApproveSettings = ({ onClick={() => { const newCommands = (deniedCommands ?? []).filter((_, i) => i !== index) setCachedStateField("deniedCommands", newCommands) - - vscode.postMessage({ - type: "updateSettings", - updatedSettings: { deniedCommands: newCommands }, - }) }}>
{cmd}
diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 3127dde363..c3af315d99 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -24,7 +24,6 @@ import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" -import { vscode } from "@/utils/vscode" type ContextManagementSettingsProps = HTMLAttributes & { autoCondenseContext: boolean @@ -139,7 +138,6 @@ export const ContextManagementSettings = ({ } setCachedStateField("profileThresholds", newThresholds) - vscode.postMessage({ type: "updateSettings", updatedSettings: { profileThresholds: newThresholds } }) } } return ( diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index 54babbcfcb..d6e0ba0f6b 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -209,10 +209,16 @@ const PromptsSettings = ({ setIncludeTaskHistoryInEnhance(target.checked) - vscode.postMessage({ - type: "updateSettings", - updatedSettings: { includeTaskHistoryInEnhance: target.checked }, - }) + // Controlled: setter buffers into cachedState (Save persists). + // Uncontrolled: context setter is local-only, so persist here. + if (!propsSetIncludeTaskHistoryInEnhance) { + vscode.postMessage({ + type: "updateSettings", + updatedSettings: { + includeTaskHistoryInEnhance: target.checked, + }, + }) + } }}> {t("prompts:supportPrompts.enhance.includeTaskHistory")} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 0b53342133..c45ca38eea 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -901,7 +901,12 @@ const SettingsView = forwardRef(({ onDone, t {renderTab === "modes" && } {/* MCP Section */} - {renderTab === "mcp" && } + {renderTab === "mcp" && ( + setCachedStateField("mcpEnabled", value)} + /> + )} {/* Worktrees Section */} {renderTab === "worktrees" && } diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx new file mode 100644 index 0000000000..8b736280ee --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx @@ -0,0 +1,100 @@ +// npx vitest src/components/settings/__tests__/AutoApproveSettings.spec.tsx + +import { render, screen, fireEvent } from "@/utils/test-utils" + +import { AutoApproveSettings } from "../AutoApproveSettings" +import { vscode } from "@/utils/vscode" + +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +// AutoApproveSettings reads a couple of live-state values that are genuinely +// immediate actions (autoApprovalEnabled). Those are out of scope for the +// Save/Discard buffering contract, so we just provide inert stand-ins. +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + autoApprovalEnabled: false, + setAutoApprovalEnabled: vi.fn(), + }), +})) + +vi.mock("@/hooks/useAutoApprovalToggles", () => ({ + useAutoApprovalToggles: () => ({}), +})) + +vi.mock("@/hooks/useAutoApprovalState", () => ({ + useAutoApprovalState: () => ({ effectiveAutoApprovalEnabled: false, hasEnabledOptions: false }), +})) + +const renderSettings = (overrides = {}) => { + const setCachedStateField = vi.fn() + const props = { + alwaysAllowExecute: true, // reveal the command list section + allowedCommands: [] as string[], + deniedCommands: [] as string[], + setCachedStateField, + ...overrides, + } + render() + return { setCachedStateField } +} + +// A change is "Save-managed" if it must NOT reach the extension host before Save. +const expectNoImmediateUpdateSettings = () => { + expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "updateSettings" })) +} + +describe("AutoApproveSettings - Save/Discard contract", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Case 1: allowedCommands add + it("buffers an added allowed command without persisting before Save", () => { + const { setCachedStateField } = renderSettings() + + fireEvent.change(screen.getByTestId("command-input"), { target: { value: "npm test" } }) + fireEvent.click(screen.getByTestId("add-command-button")) + + expect(setCachedStateField).toHaveBeenCalledWith("allowedCommands", ["npm test"]) + expectNoImmediateUpdateSettings() + }) + + // Case 2: allowedCommands remove + it("buffers a removed allowed command without persisting before Save", () => { + const { setCachedStateField } = renderSettings({ allowedCommands: ["npm test"] }) + + fireEvent.click(screen.getByTestId("remove-command-0")) + + expect(setCachedStateField).toHaveBeenCalledWith("allowedCommands", []) + expectNoImmediateUpdateSettings() + }) + + // Case 3a: deniedCommands add + it("buffers an added denied command without persisting before Save", () => { + const { setCachedStateField } = renderSettings() + + fireEvent.change(screen.getByTestId("denied-command-input"), { target: { value: "rm -rf" } }) + fireEvent.click(screen.getByTestId("add-denied-command-button")) + + expect(setCachedStateField).toHaveBeenCalledWith("deniedCommands", ["rm -rf"]) + expectNoImmediateUpdateSettings() + }) + + // Case 3b: deniedCommands remove + it("buffers a removed denied command without persisting before Save", () => { + const { setCachedStateField } = renderSettings({ deniedCommands: ["rm -rf"] }) + + fireEvent.click(screen.getByTestId("remove-denied-command-0")) + + expect(setCachedStateField).toHaveBeenCalledWith("deniedCommands", []) + expectNoImmediateUpdateSettings() + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 57b3066b4d..10d5d0fe4f 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -2,6 +2,7 @@ import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { ContextManagementSettings } from "../ContextManagementSettings" +import { vscode } from "@/utils/vscode" // Mock the translation hook vi.mock("@/hooks/useAppTranslation", () => ({ @@ -47,8 +48,16 @@ vi.mock("@/components/ui", () => ({ {children} ), - Select: ({ children, ...props }: any) => ( -
+ Select: ({ children, value, onValueChange, ...props }: any) => ( +
+ {/* Hidden trigger lets tests drive profile selection deterministically: + set data-next-value on the button, then click it. */} +
), @@ -391,6 +400,26 @@ describe("ContextManagementSettings", () => { render() expect(screen.getByText("75%")).toBeInTheDocument() }) + + // Case 4: profileThresholds must buffer through cachedState, not persist before Save. + it("buffers profile threshold changes without persisting before Save", () => { + const mockSetCachedStateField = vitest.fn() + const props = { ...autoCondenseProps, setCachedStateField: mockSetCachedStateField } + render() + + // Select a non-default profile so the slider edits profileThresholds. + const profileTrigger = screen.getByTestId("threshold-profile-change") + profileTrigger.setAttribute("data-next-value", "config-1") + fireEvent.click(profileTrigger) + + // Move the threshold slider for that profile. + const slider = screen.getByTestId("condense-threshold-slider") + slider.focus() + fireEvent.keyDown(slider, { key: "ArrowRight" }) + + expect(mockSetCachedStateField).toHaveBeenCalledWith("profileThresholds", { "config-1": 76 }) + expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "updateSettings" })) + }) }) it("handles boundary values for sliders", () => { diff --git a/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx new file mode 100644 index 0000000000..d10e100f45 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx @@ -0,0 +1,91 @@ +// npx vitest src/components/settings/__tests__/PromptsSettings.spec.tsx + +import { render, screen, fireEvent } from "@/utils/test-utils" + +import PromptsSettings from "../PromptsSettings" +import { vscode } from "@src/utils/vscode" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +// Exposed so tests can assert the context (uncontrolled) setter still fires. +const { contextSetIncludeTaskHistoryInEnhance } = vi.hoisted(() => ({ + contextSetIncludeTaskHistoryInEnhance: vi.fn(), +})) + +// PromptsSettings falls back to context when props are absent. +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + listApiConfigMeta: [], + enhancementApiConfigId: "", + setEnhancementApiConfigId: vi.fn(), + includeTaskHistoryInEnhance: true, + setIncludeTaskHistoryInEnhance: contextSetIncludeTaskHistoryInEnhance, + }), +})) + +const renderSettings = (overrides = {}) => { + const setIncludeTaskHistoryInEnhance = vi.fn() + const props = { + customSupportPrompts: {}, + setCustomSupportPrompts: vi.fn(), + includeTaskHistoryInEnhance: true, + setIncludeTaskHistoryInEnhance, + ...overrides, + } + render() + return { setIncludeTaskHistoryInEnhance } +} + +describe("PromptsSettings - Save/Discard contract", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Case 5: includeTaskHistoryInEnhance must buffer, not persist before Save. + it("buffers includeTaskHistoryInEnhance toggle without persisting before Save", () => { + const { setIncludeTaskHistoryInEnhance } = renderSettings({ includeTaskHistoryInEnhance: true }) + + // The ENHANCE support option is active by default, so the checkbox is present. + const checkbox = screen + .getByText("prompts:supportPrompts.enhance.includeTaskHistory") + .closest("label")! + .querySelector('input[type="checkbox"]')! + fireEvent.click(checkbox) + + expect(setIncludeTaskHistoryInEnhance).toHaveBeenCalledWith(false) + expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "updateSettings" })) + }) + + // Uncontrolled usage (props omitted) has no cachedState/Save wiring, so the + // toggle must persist immediately via updateSettings — otherwise it is lost. + it("persists includeTaskHistoryInEnhance immediately when used uncontrolled", () => { + render( + , + ) + + const checkbox = screen + .getByText("prompts:supportPrompts.enhance.includeTaskHistory") + .closest("label")! + .querySelector('input[type="checkbox"]')! + fireEvent.click(checkbox) + + // Still updates local (context) state, and also persists immediately. + expect(contextSetIncludeTaskHistoryInEnhance).toHaveBeenCalledWith(false) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { includeTaskHistoryInEnhance: false }, + }) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 6434eadb23..f4defb87dd 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -629,13 +629,13 @@ describe("SettingsView - Allowed Commands", () => { // Verify command was added expect(within(content).getByText("npm test")).toBeInTheDocument() - // Verify VSCode message was sent - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "updateSettings", - updatedSettings: { - allowedCommands: ["npm test"], - }, - }) + // Adding a command must NOT persist before Save; it only buffers in cachedState. + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ allowedCommands: ["npm test"] }), + }), + ) }) it("removes command from the list", () => { @@ -663,13 +663,13 @@ describe("SettingsView - Allowed Commands", () => { // Verify command was removed expect(within(content).queryByText("npm test")).not.toBeInTheDocument() - // Verify VSCode message was sent - expect(vscode.postMessage).toHaveBeenLastCalledWith({ - type: "updateSettings", - updatedSettings: { - allowedCommands: [], - }, - }) + // Removing a command must NOT persist before Save; it only buffers in cachedState. + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ allowedCommands: expect.anything() }), + }), + ) }) describe("SettingsView - Tab Navigation", () => { @@ -776,4 +776,46 @@ describe("SettingsView - Duplicate Commands", () => { }), ) }) + + it("reverts and does not persist allowed commands when discarding unsaved changes", () => { + // Render once and get the activateTab helper + const { onDone, activateTab, getSettingsContent } = renderSettingsView() + + // Activate the autoApprove tab + activateTab("autoApprove") + + const content = getSettingsContent() + // Enable always allow execute + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") + fireEvent.click(executeCheckbox) + + // Add a command (buffers into cachedState, must not persist yet) + const input = within(content).getByTestId("command-input") + fireEvent.change(input, { target: { value: "npm test" } }) + const addButton = within(content).getByTestId("add-command-button") + fireEvent.click(addButton) + + // Verify the command was buffered and rendered before discarding it + expect(within(content).getByText("npm test")).toBeInTheDocument() + + // Click Done, which opens the unsaved-changes dialog since a change is detected + const doneButton = screen.getByText("settings:common.done") + fireEvent.click(doneButton) + + // Confirm discard + const discardButton = screen.getByTestId("alert-dialog-action") + fireEvent.click(discardButton) + + // Discarding must never persist the buffered command edit + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ allowedCommands: ["npm test"] }), + }), + ) + + // The buffered edit must be reverted before leaving Settings + expect(within(getSettingsContent()).queryByText("npm test")).not.toBeInTheDocument() + expect(onDone).toHaveBeenCalledTimes(1) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 567838cc30..88428a077d 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -4,13 +4,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import React from "react" import SettingsView from "../SettingsView" +import { vscode } from "@src/utils/vscode" -// Mock vscode API -const mockPostMessage = vi.fn() -const mockVscode = { - postMessage: mockPostMessage, -} -;(global as any).acquireVsCodeApi = () => mockVscode +const postMessage = vi.spyOn(vscode, "postMessage").mockImplementation(() => {}) // Mock the extension state context vi.mock("@src/context/ExtensionStateContext", () => ({ @@ -170,7 +166,14 @@ vi.mock("@src/components/modes/ModesView", () => ({ })) vi.mock("@src/components/mcp/McpView", () => ({ - default: () => null, + default: ({ mcpEnabled, setMcpEnabled }: any) => ( + + ), })) // Mock Tab components @@ -596,4 +599,33 @@ describe("SettingsView - Unsaved Changes Detection", () => { // No dialog should appear expect(screen.queryByText("settings:unsavedChangesDialog.title")).not.toBeInTheDocument() }) + + it("buffers MCP enablement until Save", async () => { + render( + + + , + ) + + const toggle = await screen.findByTestId("mcp-enabled-toggle") + fireEvent.click(toggle) + + await waitFor(() => expect(toggle).toHaveAttribute("data-mcp-enabled", "true")) + expect(postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ mcpEnabled: true }), + }), + ) + + await waitFor(() => expect(screen.getByTestId("save-button")).toBeEnabled()) + fireEvent.click(screen.getByTestId("save-button")) + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ mcpEnabled: true }), + }), + ) + }) }) From 2d2a2384d10598b285b8cf88131dfc09552f01e0 Mon Sep 17 00:00:00 2001 From: Naved Merchant Date: Sat, 25 Jul 2026 05:57:58 -0700 Subject: [PATCH 58/89] fix(ollama): always respond to model refresh, surface errors, and use form-edited base URL (#878) * ollama fetch fix * address review comments --- .../__tests__/webviewMessageHandler.spec.ts | 94 +++++++++ src/core/webview/webviewMessageHandler.ts | 47 ++++- .../src/components/settings/ApiOptions.tsx | 9 +- .../components/settings/providers/Ollama.tsx | 74 ++++++- .../providers/__tests__/Ollama.spec.tsx | 192 +++++++++++++++++- 5 files changed, 385 insertions(+), 31 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 26aeeb2f1d..d843a3a6d9 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -294,6 +294,9 @@ describe("webviewMessageHandler - image mentions", () => { describe("webviewMessageHandler - requestOllamaModels", () => { beforeEach(() => { vi.clearAllMocks() + mockFlushModels.mockReset() + mockFlushModels.mockResolvedValue(undefined) + mockGetModels.mockReset() mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { ollamaModelId: "model-1", @@ -331,6 +334,97 @@ describe("webviewMessageHandler - requestOllamaModels", () => { ollamaModels: mockModels, }) }) + + it("posts empty models response when no models are found", async () => { + mockGetModels.mockResolvedValue({}) + + await webviewMessageHandler(mockClineProvider, { + type: "requestOllamaModels", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "ollamaModels", + ollamaModels: {}, + }) + }) + + it("posts empty models response with error message and logs to output on fetch failure", async () => { + mockGetModels.mockRejectedValue(new Error("Connection refused")) + + await webviewMessageHandler(mockClineProvider, { + type: "requestOllamaModels", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "ollamaModels", + ollamaModels: {}, + error: "Connection refused", + }) + + expect(mockClineProvider.log).toHaveBeenCalledWith( + "[requestOllamaModels] Failed to read models for http://localhost:1234: Connection refused", + ) + }) + + it("distinguishes a model cache refresh failure from a model read failure", async () => { + mockFlushModels.mockRejectedValue(new Error("Cache write failed")) + + await webviewMessageHandler(mockClineProvider, { + type: "requestOllamaModels", + values: { baseUrl: "https://ollama.example.com" }, + }) + + expect(mockGetModels).not.toHaveBeenCalled() + expect(mockClineProvider.log).toHaveBeenCalledWith( + "[requestOllamaModels] Failed to refresh model cache for https://ollama.example.com: Cache write failed", + ) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "ollamaModels", + ollamaModels: {}, + error: "Cache write failed", + }) + }) + + it("uses baseUrl from message values over saved state", async () => { + const mockModels: ModelRecord = { + "remote-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Remote model", + }, + } + + mockGetModels.mockResolvedValue(mockModels) + + await webviewMessageHandler(mockClineProvider, { + type: "requestOllamaModels", + values: { + baseUrl: "https://ollama.example.com", + apiKey: "secret-key", + }, + }) + + // Should use the URL from message values, not the saved state + expect(mockFlushModels).toHaveBeenCalledWith( + { + provider: "ollama", + baseUrl: "https://ollama.example.com", + apiKey: "secret-key", + }, + true, + ) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "ollama", + baseUrl: "https://ollama.example.com", + apiKey: "secret-key", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "ollamaModels", + ollamaModels: mockModels, + }) + }) }) describe("webviewMessageHandler - requestRouterModels", () => { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 02f8602b5a..a72c4955a3 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1263,23 +1263,48 @@ export const webviewMessageHandler = async ( case "requestOllamaModels": { // Specific handler for Ollama models only. const { apiConfiguration: ollamaApiConfig } = await provider.getState() + // Prefer the baseUrl/apiKey from the message values (which reflect + // the user's unsaved edits in the settings form) over the saved + // state, so the refresh uses the URL the user is actually looking + // at — not the stale one from before they started editing. + const baseUrl = message.values?.baseUrl ?? ollamaApiConfig.ollamaBaseUrl + const apiKey = message.values?.apiKey ?? ollamaApiConfig.ollamaApiKey + const logBaseUrl = baseUrl || "http://localhost:11434" + const ollamaOptions = { + provider: "ollama" as const, + baseUrl, + apiKey, + } try { - const ollamaOptions = { - provider: "ollama" as const, - baseUrl: ollamaApiConfig.ollamaBaseUrl, - apiKey: ollamaApiConfig.ollamaApiKey, - } - // Flush cache and refresh to ensure fresh models. + // Refresh the cache before reading the models. Keep this error + // separate from the read below so diagnostics identify which + // cache operation failed. await flushModels(ollamaOptions, true) + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + provider.log(`[requestOllamaModels] Failed to refresh model cache for ${logBaseUrl}: ${errorMsg}`) + provider.postMessageToWebview({ + type: "ollamaModels", + ollamaModels: {}, + error: errorMsg, + }) + break + } + try { const ollamaModels = await getModels(ollamaOptions) - if (Object.keys(ollamaModels).length > 0) { - provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels }) - } + // Always post a response so the webview refresh status can + // transition out of "loading" — even when no models are found. + provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) } catch (error) { - // Silently fail - user hasn't configured Ollama yet - console.debug("Ollama models fetch failed:", error) + const errorMsg = error instanceof Error ? error.message : String(error) + provider.log(`[requestOllamaModels] Failed to read models for ${logBaseUrl}: ${errorMsg}`) + provider.postMessageToWebview({ + type: "ollamaModels", + ollamaModels: {}, + error: errorMsg, + }) } break } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index d38f49f958..c5e69978ff 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -221,7 +221,13 @@ const ApiOptions = ({ }, }) } else if (selectedProvider === "ollama") { - vscode.postMessage({ type: "requestOllamaModels" }) + vscode.postMessage({ + type: "requestOllamaModels", + values: { + baseUrl: apiConfiguration?.ollamaBaseUrl, + apiKey: apiConfiguration?.ollamaApiKey, + }, + }) } else if (selectedProvider === "lmstudio") { requestLmStudioModels(apiConfiguration?.lmStudioBaseUrl) } else if (selectedProvider === "vscode-lm") { @@ -245,6 +251,7 @@ const ApiOptions = ({ apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, apiConfiguration?.ollamaBaseUrl, + apiConfiguration?.ollamaApiKey, apiConfiguration?.lmStudioBaseUrl, apiConfiguration?.litellmBaseUrl, apiConfiguration?.litellmApiKey, diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx index 9d92ee972b..9b11c85369 100644 --- a/webview-ui/src/components/settings/providers/Ollama.tsx +++ b/webview-ui/src/components/settings/providers/Ollama.tsx @@ -1,5 +1,4 @@ -import { useState, useCallback, useMemo, useEffect } from "react" -import { useEvent } from "react-use" +import { useState, useCallback, useMemo, useEffect, useRef } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { Checkbox } from "vscrui" @@ -7,6 +6,7 @@ import { type ProviderSettings, type ExtensionMessage, type ModelRecord, ollamaD import { useAppTranslation } from "@src/i18n/TranslationContext" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" +import { Button } from "@src/components/ui" import { vscode } from "@src/utils/vscode" import { inputEventTransform } from "../transforms" @@ -22,6 +22,9 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro const { t } = useAppTranslation() const [ollamaModels, setOllamaModels] = useState({}) + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const refreshStatusRef = useRef(refreshStatus) const routerModels = useRouterModels() const handleInputChange = useCallback( @@ -35,20 +38,42 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro [setApiConfigurationField], ) - const onMessage = useCallback((event: MessageEvent) => { - const message: ExtensionMessage = event.data + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message: ExtensionMessage = event.data + + if (message.type === "ollamaModels") { + if (!message.error) { + setOllamaModels(message.ollamaModels ?? {}) + } - switch (message.type) { - case "ollamaModels": - { - const newModels = message.ollamaModels ?? {} - setOllamaModels(newModels) + if (refreshStatusRef.current === "loading") { + const nextStatus = message.error ? "error" : "success" + refreshStatusRef.current = nextStatus + setRefreshStatus(nextStatus) + setRefreshError(message.error) } - break + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) } }, []) - useEvent("message", onMessage) + const handleRefreshModels = useCallback(() => { + refreshStatusRef.current = "loading" + setRefreshStatus("loading") + setRefreshError(undefined) + vscode.postMessage({ + type: "requestOllamaModels", + values: { + baseUrl: apiConfiguration?.ollamaBaseUrl, + apiKey: apiConfiguration?.ollamaApiKey, + }, + }) + }, [apiConfiguration?.ollamaBaseUrl, apiConfiguration?.ollamaApiKey]) // Refresh models on mount useEffect(() => { @@ -102,6 +127,33 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
)} + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading")} +
+ )} + {refreshStatus === "success" && ( +
{t("settings:providers.refreshModels.success")}
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error")} +
+ )} ({ // Mock the ModelPicker vi.mock("../../ModelPicker", () => ({ - ModelPicker: () =>
Model Picker
, + ModelPicker: ({ models }: any) =>
{Object.keys(models ?? {}).join(",")}
, })) // Mock the ThinkingBudget @@ -57,21 +57,33 @@ vi.mock("../../ThinkingBudget", () => ({ ), })) -// Mock react-use -vi.mock("react-use", () => ({ - useEvent: vi.fn(), -})) - // Mock useRouterModels vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ useRouterModels: () => ({ data: {}, isLoading: false, error: null }), })) +const { postMessageMock } = vi.hoisted(() => ({ + postMessageMock: vi.fn(), +})) + // Mock vscode vi.mock("@src/utils/vscode", () => ({ - vscode: { postMessage: vi.fn() }, + vscode: { postMessage: postMessageMock }, })) +// Stub the shared Button so we can assert onClick/disabled without its styling deps. +vi.mock("@src/components/ui", async () => { + const actual = await vi.importActual("@src/components/ui") + return { + ...actual, + Button: ({ children, onClick, disabled, className }: any) => ( + + ), + } +}) + describe("Ollama Component - thinking setting", () => { const mockSetApiConfigurationField = vi.fn() @@ -221,3 +233,167 @@ describe("Ollama Component - thinking setting", () => { expect(screen.queryByTestId("thinking-budget")).toBeNull() }) }) + +describe("Ollama Component - refresh models", () => { + const mockSetApiConfigurationField = vi.fn() + + const dispatchMessage = (data: any) => + act(() => { + window.dispatchEvent(new MessageEvent("message", { data })) + }) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders the refresh button in idle state", () => { + render( + , + ) + + const button = screen.getByTestId("refresh-button") + expect(button).not.toBeDisabled() + expect(button.querySelector(".codicon-refresh")).not.toBeNull() + expect(screen.getByText("settings:providers.refreshModels.label")).toBeInTheDocument() + }) + + it("sends requestOllamaModels with baseUrl and apiKey when the refresh button is clicked", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + + expect(postMessageMock).toHaveBeenCalledWith({ + type: "requestOllamaModels", + values: { + baseUrl: "https://ollama.example.com", + apiKey: "secret-key", + }, + }) + }) + + it("enters loading state and disables the button while refreshing", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + + const button = screen.getByTestId("refresh-button") + expect(button).toBeDisabled() + expect(button.querySelector(".codicon-loading")).not.toBeNull() + expect(screen.getByText("settings:providers.refreshModels.loading")).toBeInTheDocument() + }) + + it("handles a response dispatched synchronously while posting the refresh request", () => { + render( + , + ) + + // Ignore the mount request and respond synchronously to the explicit refresh. + postMessageMock.mockClear() + postMessageMock.mockImplementationOnce(() => { + window.dispatchEvent( + new MessageEvent("message", { data: { type: "ollamaModels", ollamaModels: { "llama3:latest": {} } } }), + ) + }) + fireEvent.click(screen.getByTestId("refresh-button")) + + expect(screen.getByText("settings:providers.refreshModels.success")).toBeInTheDocument() + expect(screen.getByTestId("model-picker")).toHaveTextContent("llama3:latest") + }) + + it("shows success state when ollamaModels arrives with models while loading", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + dispatchMessage({ type: "ollamaModels", ollamaModels: { "llama3:latest": {} } }) + + expect(screen.getByText("settings:providers.refreshModels.success")).toBeInTheDocument() + }) + + it("shows success state when Ollama is reachable but has no installed models", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + dispatchMessage({ type: "ollamaModels", ollamaModels: {} }) + + expect(screen.getByText("settings:providers.refreshModels.success")).toBeInTheDocument() + }) + + it("displays the backend error message when ollamaModels arrives with an error", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + dispatchMessage({ type: "ollamaModels", ollamaModels: {}, error: "Connection refused" }) + + expect(screen.getByText("Connection refused")).toBeInTheDocument() + }) + + it("preserves previously loaded models when a later refresh fails", () => { + render( + , + ) + + fireEvent.click(screen.getByTestId("refresh-button")) + dispatchMessage({ type: "ollamaModels", ollamaModels: { "llama3:latest": {} } }) + expect(screen.getByTestId("model-picker")).toHaveTextContent("llama3:latest") + + fireEvent.click(screen.getByTestId("refresh-button")) + dispatchMessage({ type: "ollamaModels", ollamaModels: {}, error: "Connection refused" }) + + expect(screen.getByText("Connection refused")).toBeInTheDocument() + expect(screen.getByTestId("model-picker")).toHaveTextContent("llama3:latest") + }) + + it("ignores ollamaModels messages when not in loading state", () => { + render( + , + ) + + // No refresh initiated; an unsolicited ollamaModels message should be a no-op. + dispatchMessage({ type: "ollamaModels", ollamaModels: { "llama3:latest": {} } }) + + expect(screen.queryByText("settings:providers.refreshModels.success")).not.toBeInTheDocument() + expect(screen.queryByText("settings:providers.refreshModels.loading")).not.toBeInTheDocument() + }) +}) From 4e9543f59ce7caec3423385b41db695eb5f5add8 Mon Sep 17 00:00:00 2001 From: LouisClt Date: Sat, 25 Jul 2026 20:04:57 +0200 Subject: [PATCH 59/89] Fix bedrock DNS resolution when behind a corporate proxy (#906) * Fix bedrock DNS resolution when behind a corporate proxy When behind a corporate proxy, Node.js resolves DNS locally before contacting the proxy. If the proxy is the only path to the endpoint, the request fails with ENOTFOUND. HttpsProxyAgent uses CONNECT tunneling so the proxy handles DNS resolution. Configure NodeHttpHandler with HttpsProxyAgent when a system proxy is detected. * test: add coverage for getSystemProxyUrl function * fix: honor NO_PROXY, support http proxy, trim proxy env vars --- .changeset/itchy-moles-thank.md | 5 + pnpm-lock.yaml | 9 ++ src/api/providers/__tests__/bedrock.spec.ts | 113 +++++++++++++ src/api/providers/bedrock.ts | 23 +++ src/package.json | 3 + src/utils/__tests__/networkProxy.spec.ts | 166 +++++++++++++++++++- src/utils/networkProxy.ts | 73 +++++++++ 7 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-moles-thank.md diff --git a/.changeset/itchy-moles-thank.md b/.changeset/itchy-moles-thank.md new file mode 100644 index 0000000000..42bb637024 --- /dev/null +++ b/.changeset/itchy-moles-thank.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Fix bedrock DNS resolution when behind corporate proxy diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e7150a577..af4027db37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,6 +466,9 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../packages/types + '@smithy/node-http-handler': + specifier: ^4.0.0 + version: 4.9.3 ai-sdk-provider-poe: specifier: 2.0.18 version: 2.0.18(ai@6.0.218(zod@3.25.76))(zod@3.25.76) @@ -514,6 +517,12 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + http-proxy-agent: + specifier: ^7.0.0 + version: 7.0.2 + https-proxy-agent: + specifier: ^7.0.0 + version: 7.0.6 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.9.3) diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 35856e8705..71c2d1cd0c 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -18,6 +18,22 @@ vi.mock("@aws-sdk/credential-providers", () => { return { fromIni: mockFromIni } }) +vi.mock("../../../utils/networkProxy", () => ({ + getSystemProxyUrl: vi.fn().mockReturnValue(undefined), +})) + +vi.mock("@smithy/node-http-handler", () => ({ + NodeHttpHandler: vi.fn(), +})) + +vi.mock("http-proxy-agent", () => ({ + HttpProxyAgent: vi.fn(), +})) + +vi.mock("https-proxy-agent", () => ({ + HttpsProxyAgent: vi.fn(), +})) + // Mock BedrockRuntimeClient and ConverseStreamCommand vi.mock("@aws-sdk/client-bedrock-runtime", () => { const mockSend = vi.fn().mockResolvedValue({ @@ -46,10 +62,18 @@ import { } from "@roo-code/types" import type { Anthropic } from "@anthropic-ai/sdk" +import { getSystemProxyUrl } from "../../../utils/networkProxy" +import { NodeHttpHandler } from "@smithy/node-http-handler" +import { HttpProxyAgent } from "http-proxy-agent" +import { HttpsProxyAgent } from "https-proxy-agent" // Get access to the mocked functions const mockConverseStreamCommand = vi.mocked(ConverseStreamCommand) const mockBedrockRuntimeClient = vi.mocked(BedrockRuntimeClient) +const mockGetSystemProxyUrl = vi.mocked(getSystemProxyUrl) +const mockNodeHttpHandler = vi.mocked(NodeHttpHandler) +const mockHttpProxyAgent = vi.mocked(HttpProxyAgent) +const mockHttpsProxyAgent = vi.mocked(HttpsProxyAgent) describe("AwsBedrockHandler", () => { let handler: AwsBedrockHandler @@ -118,6 +142,95 @@ describe("AwsBedrockHandler", () => { }) }) + describe("proxy configuration", () => { + afterEach(() => { + mockGetSystemProxyUrl.mockReturnValue(undefined) + }) + + it("should configure NodeHttpHandler with HttpProxyAgent and HttpsProxyAgent when proxy URL is set", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + // Verify both proxy agents were created with the correct URL + expect(mockHttpProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") + expect(mockHttpsProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") + + // Verify NodeHttpHandler was created with both agents + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpAgent: expect.anything(), + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + + // Verify requestHandler was set on BedrockRuntimeClient config + expect(mockBedrockRuntimeClient).toHaveBeenLastCalledWith( + expect.objectContaining({ requestHandler: expect.anything() }), + ) + }) + + it("should not create a proxy requestHandler when no proxy is configured", () => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).not.toHaveBeenCalled() + expect(mockBedrockRuntimeClient.mock.lastCall?.[0]?.requestHandler).toBeUndefined() + }) + + it("should apply proxy for API key authentication", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsUseApiKey: true, + awsApiKey: "test-api-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + }) + + it("should pass a custom endpoint to getSystemProxyUrl for NO_PROXY matching", () => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + awsBedrockEndpoint: "https://bedrock.vpce.internal", + awsBedrockEndpointEnabled: true, + }) + + expect(mockGetSystemProxyUrl).toHaveBeenCalledWith("https://bedrock.vpce.internal") + }) + + it("should pass undefined to getSystemProxyUrl when no custom endpoint is set", () => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockGetSystemProxyUrl).toHaveBeenCalledWith(undefined) + }) + }) + describe("region mapping and cross-region inference", () => { describe("getPrefixForRegion", () => { it("should return correct prefix for US regions", () => { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 2981ed0149..645ad8d354 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -10,9 +10,12 @@ import { ToolConfiguration, ToolChoice, } from "@aws-sdk/client-bedrock-runtime" +import { NodeHttpHandler } from "@smithy/node-http-handler" import OpenAI from "openai" import { fromIni } from "@aws-sdk/credential-providers" import { Anthropic } from "@anthropic-ai/sdk" +import { HttpProxyAgent } from "http-proxy-agent" +import { HttpsProxyAgent } from "https-proxy-agent" import { type ModelInfo, @@ -44,6 +47,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor import { getModelParams } from "../transform/model-params" import { shouldUseReasoningBudget } from "../../shared/api" import { normalizeToolSchema } from "../../utils/json-schema" +import { getSystemProxyUrl } from "../../utils/networkProxy" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" /************************************************************************************ @@ -294,6 +298,25 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } + // When a corporate proxy is configured, Node resolves DNS locally before tunneling, + // causing ENOTFOUND for endpoints that only the proxy can reach. HttpProxyAgent and + // HttpsProxyAgent use CONNECT tunneling so the proxy handles DNS resolution instead. + // + // A custom endpoint (e.g. a VPC endpoint) is passed so NO_PROXY can bypass the proxy + // for directly-reachable hosts. For the default managed endpoint we don't reconstruct + // the hostname (the AWS SDK resolves it internally, and it varies by partition), so the + // proxy always applies there. + const proxyUrl = getSystemProxyUrl( + typeof clientConfig.endpoint === "string" ? clientConfig.endpoint : undefined, + ) + if (proxyUrl) { + clientConfig.requestHandler = new NodeHttpHandler({ + httpAgent: new HttpProxyAgent(proxyUrl), + httpsAgent: new HttpsProxyAgent(proxyUrl), + requestTimeout: 0, + }) + } + this.client = new BedrockRuntimeClient(clientConfig) } diff --git a/src/package.json b/src/package.json index eea27ba3f9..31b6c31bca 100644 --- a/src/package.json +++ b/src/package.json @@ -461,6 +461,7 @@ "@anthropic-ai/vertex-sdk": "^0.19.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", "@aws-sdk/credential-providers": "^3.922.0", + "@smithy/node-http-handler": "^4.0.0", "@google/genai": "^1.29.1", "@lmstudio/sdk": "^1.1.1", "@mistralai/mistralai": "^1.9.18", @@ -486,6 +487,8 @@ "get-folder-size": "^5.0.0", "global-agent": "^3.0.0", "google-auth-library": "^10.2.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", diff --git a/src/utils/__tests__/networkProxy.spec.ts b/src/utils/__tests__/networkProxy.spec.ts index 97c046d1b0..94e91cd990 100644 --- a/src/utils/__tests__/networkProxy.spec.ts +++ b/src/utils/__tests__/networkProxy.spec.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import { initializeNetworkProxy, getProxyConfig, isProxyEnabled, isDebugMode } from "../networkProxy" +import { initializeNetworkProxy, getProxyConfig, isProxyEnabled, isDebugMode, getSystemProxyUrl } from "../networkProxy" // Mock global-agent vi.mock("global-agent", () => ({ @@ -305,4 +305,168 @@ describe("networkProxy", () => { expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined() }) }) + + describe("getSystemProxyUrl", () => { + beforeEach(() => { + vi.clearAllMocks() + // Clear all proxy env vars and VS Code setting before each test + delete process.env.HTTPS_PROXY + delete process.env.https_proxy + delete process.env.HTTP_PROXY + delete process.env.http_proxy + delete process.env.NO_PROXY + delete process.env.no_proxy + mockConfig.get.mockReturnValue(undefined) + }) + + it("should return proxy from HTTPS_PROXY env var", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should return proxy from https_proxy env var when HTTPS_PROXY not set", () => { + process.env.https_proxy = "http://proxy.corp:3128" + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should return proxy from HTTP_PROXY env var as fallback", () => { + process.env.HTTP_PROXY = "http://proxy.corp:8080" + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:8080") + }) + + it("should return trimmed proxy from VS Code setting", () => { + mockConfig.get.mockImplementation((key: string) => { + if (key === "proxy") return " http://proxy.corp:3128 " + return "" + }) + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should return undefined when no proxy configured", () => { + mockConfig.get.mockReturnValue(undefined) + const result = getSystemProxyUrl() + expect(result).toBeUndefined() + }) + + it("should handle VS Code API errors gracefully", () => { + vi.mocked(vscode.workspace.getConfiguration).mockImplementation(() => { + throw new Error("VS Code API unavailable") + }) + const result = getSystemProxyUrl() + expect(result).toBeUndefined() + }) + + it("should not return empty string proxy", () => { + mockConfig.get.mockReturnValue("") + const result = getSystemProxyUrl() + expect(result).toBeUndefined() + }) + + it("should prioritize env vars over VS Code settings", () => { + process.env.HTTPS_PROXY = "http://env-proxy:3128" + mockConfig.get.mockReturnValue("http://vscode-proxy:8080") + const result = getSystemProxyUrl() + expect(result).toBe("http://env-proxy:3128") + }) + + it("should trim whitespace from env var proxy values", () => { + process.env.HTTPS_PROXY = " http://proxy.corp:3128 " + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should reject whitespace-only proxy values", () => { + process.env.HTTPS_PROXY = " " + const result = getSystemProxyUrl() + expect(result).toBeUndefined() + }) + + it("should skip empty env var and try next fallback", () => { + process.env.HTTP_PROXY = " http://http-proxy:3128 " + const result = getSystemProxyUrl() + expect(result).toBe("http://http-proxy:3128") + }) + + it("should use VS Code setting when all env vars are empty", () => { + mockConfig.get.mockReturnValue(" http://vscode-proxy:8080 ") + const result = getSystemProxyUrl() + expect(result).toBe("http://vscode-proxy:8080") + }) + + describe("NO_PROXY handling", () => { + it("should bypass proxy when NO_PROXY exactly matches the target host", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "bedrock.vpce.internal" + const result = getSystemProxyUrl("https://bedrock.vpce.internal") + expect(result).toBeUndefined() + }) + + it("should bypass proxy when NO_PROXY is a domain suffix of the target host", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "amazonaws.com" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBeUndefined() + }) + + it("should bypass proxy for all hosts when NO_PROXY is '*'", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "*" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBeUndefined() + }) + + it("should use the proxy when NO_PROXY does not match the target host", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "example.com" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should handle leading dot and trailing port in NO_PROXY entries", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = ".amazonaws.com:443" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBeUndefined() + }) + + it("should match one entry out of a comma-separated NO_PROXY list", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "example.com, amazonaws.com, other.net" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBeUndefined() + }) + + it("should use the proxy when no entry in a comma-separated NO_PROXY list matches", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "example.com, foo.net, other.org" + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should also bypass the VS Code proxy when NO_PROXY matches", () => { + process.env.NO_PROXY = "amazonaws.com" + mockConfig.get.mockReturnValue("http://vscode-proxy:8080") + const result = getSystemProxyUrl("https://bedrock-runtime.us-east-1.amazonaws.com") + expect(result).toBeUndefined() + }) + + it("should ignore NO_PROXY when no target URL is provided", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "*" + const result = getSystemProxyUrl() + expect(result).toBe("http://proxy.corp:3128") + }) + + it("should not bypass when target URL is malformed", () => { + process.env.HTTPS_PROXY = "http://proxy.corp:3128" + process.env.NO_PROXY = "amazonaws.com" + const result = getSystemProxyUrl("not-a-valid-url") + expect(result).toBe("http://proxy.corp:3128") + }) + }) + }) }) diff --git a/src/utils/networkProxy.ts b/src/utils/networkProxy.ts index 448bc1b576..835887334e 100644 --- a/src/utils/networkProxy.ts +++ b/src/utils/networkProxy.ts @@ -346,6 +346,79 @@ export function isDebugMode(): boolean { return extensionContext.extensionMode === vscode.ExtensionMode.Development } +/** + * Determine whether a target URL should bypass the proxy based on NO_PROXY / no_proxy. + * + * Follows the widely-used convention (curl, proxy-from-env): + * - `NO_PROXY=*` bypasses the proxy for every host. + * - Entries are comma-separated host suffixes; an optional leading `.` or `*.` and a + * trailing `:port` are ignored. A target matches when its hostname equals an entry + * or ends with `.` (so `amazonaws.com` covers `bedrock-runtime.us-east-1.amazonaws.com`). + * - Matching is case-insensitive. + */ +function isNoProxyHost(targetUrl: string): boolean { + const noProxy = (process.env.NO_PROXY || process.env.no_proxy || "").trim() + if (!noProxy) return false + if (noProxy === "*") return true + + let hostname: string + try { + hostname = new URL(targetUrl).hostname.toLowerCase() + } catch { + return false + } + if (!hostname) return false + + return noProxy + .split(",") + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean) + .some((entry) => { + // Normalize a leading wildcard/dot and a trailing port: "*.example.com:443" -> "example.com" + const suffix = entry.replace(/^\*?\./, "").replace(/:\d+$/, "") + if (!suffix) return false + return hostname === suffix || hostname.endsWith(`.${suffix}`) + }) +} + +/** + * Get the proxy URL from environment variables or VS Code settings. + * Works in all extension modes (production and debug). + * + * When `targetUrl` is provided and its host is covered by NO_PROXY / no_proxy, this + * returns undefined so the caller connects directly — honoring the user's intent to + * bypass the proxy for e.g. a directly-reachable VPC or AWS endpoint. + * + * @param targetUrl Optional URL of the request destination, used for NO_PROXY matching. + * @returns The proxy URL, or undefined if no proxy applies or only whitespace is set. + */ +export function getSystemProxyUrl(targetUrl?: string): string | undefined { + // If the destination is covered by NO_PROXY, connect directly regardless of proxy source. + if (targetUrl && isNoProxyHost(targetUrl)) { + return undefined + } + + // Standard proxy environment variables (HTTPS takes precedence over HTTP). + // Trim to reject whitespace-only values which would be invalid for HttpsProxyAgent. + const fromEnv = + process.env.HTTPS_PROXY?.trim() || + process.env.https_proxy?.trim() || + process.env.HTTP_PROXY?.trim() || + process.env.http_proxy?.trim() + + if (fromEnv) return fromEnv + + // Fall back to VS Code's http.proxy setting + try { + const vsCodeProxy = vscode.workspace.getConfiguration("http").get("proxy") + if (vsCodeProxy?.trim()) return vsCodeProxy.trim() + } catch { + // VS Code API may be unavailable in test environments + } + + return undefined +} + /** * Log a message to the output channel if available. */ From e479b7ee0ceb500d5e652c0840d0a81ebd1e1fab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:12:58 -0400 Subject: [PATCH 60/89] chore: update Node.js to v22 LTS (22.23.1) (#743) * chore(deps): update node.js to v24 * chore(deps): target node 22 lts * chore: downgrade Node to 22 LTS to fix Windows CI crash --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- .github/actions/setup-node-pnpm/action.yml | 2 +- .nvmrc | 2 +- .tool-versions | 2 +- apps/cli/package.json | 2 +- apps/vscode-e2e/package.json | 2 +- package.json | 4 +- packages/build/package.json | 2 +- packages/cloud/package.json | 2 +- packages/core/package.json | 2 +- packages/ipc/package.json | 2 +- packages/telemetry/package.json | 2 +- packages/types/package.json | 2 +- packages/vscode-shim/package.json | 2 +- pnpm-lock.yaml | 132 ++++++++++----------- renovate.json | 5 + src/package.json | 4 +- webview-ui/package.json | 2 +- 17 files changed, 88 insertions(+), 83 deletions(-) diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index 8535881ee5..a05fdb65e5 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -6,7 +6,7 @@ inputs: node-version: description: "Node.js version to use" required: false - default: "20.20.2" + default: "22.23.1" pnpm-version: description: "pnpm version to use" required: false diff --git a/.nvmrc b/.nvmrc index ccc4c6c7f8..f9e7451e77 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.20.2 +22.23.1 diff --git a/.tool-versions b/.tool-versions index 550032bc20..36731124c7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ pnpm 10.8.1 -nodejs 20.20.2 +nodejs 22.23.1 diff --git a/apps/cli/package.json b/apps/cli/package.json index 04a59a8162..7e0b78827d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -41,7 +41,7 @@ "@roo-code/config-eslint": "workspace:^", "@trpc/server": "^11.18.0", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/react": "18.3.31", "@vitest/coverage-v8": "4.1.9", "ink-testing-library": "4.0.0", diff --git a/apps/vscode-e2e/package.json b/apps/vscode-e2e/package.json index 2662e7cf07..3b1a3fb18f 100644 --- a/apps/vscode-e2e/package.json +++ b/apps/vscode-e2e/package.json @@ -17,7 +17,7 @@ "@roo-code/types": "workspace:^", "@copilotkit/aimock": "1.35.0", "@types/mocha": "10.0.10", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/vscode": "1.100.0", "@vscode/test-electron": "2.5.2", "dotenv-cli": "11.0.0", diff --git a/package.json b/package.json index 2c1dd77639..b2ab5b7173 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "roo-code", "packageManager": "pnpm@10.8.1", "engines": { - "node": "20.20.2" + "node": "22.23.1" }, "scripts": { "preinstall": "node scripts/bootstrap.mjs", @@ -30,7 +30,7 @@ "devDependencies": { "@changesets/cli": "2.31.0", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@vscode/vsce": "3.9.2", "esbuild": "0.28.1", "eslint": "9.39.4", diff --git a/packages/build/package.json b/packages/build/package.json index e0175a38d3..f9d5d352ff 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "vitest": "4.1.9" } } diff --git a/packages/cloud/package.json b/packages/cloud/package.json index c6cb23c2c0..8757262bb2 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/vscode": "1.100.0", "globals": "16.3.0", "@vitest/coverage-v8": "4.1.9", diff --git a/packages/core/package.json b/packages/core/package.json index 16883a6430..741a71704a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@vitest/coverage-v8": "4.1.9", "vitest": "4.1.9" } diff --git a/packages/ipc/package.json b/packages/ipc/package.json index d79e8b5900..374c4240b6 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/node-ipc": "9.2.3" } } diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index c7be0214fe..6c45195f6c 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/vscode": "1.100.0", "@vitest/coverage-v8": "4.1.9", "vitest": "4.1.9" diff --git a/packages/types/package.json b/packages/types/package.json index 0f5cf6187b..c4cbd4454c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "globals": "16.3.0", "tsup": "8.5.1", "ajv": "8.20.0", diff --git a/packages/vscode-shim/package.json b/packages/vscode-shim/package.json index c366384104..167ec333d1 100644 --- a/packages/vscode-shim/package.json +++ b/packages/vscode-shim/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "vitest": "4.1.9" }, "dependencies": {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af4027db37..7513d35d27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,13 +24,13 @@ importers: devDependencies: '@changesets/cli': specifier: 2.31.0 - version: 2.31.0(@types/node@20.19.43) + version: 2.31.0(@types/node@22.20.1) '@roo-code/config-typescript': specifier: workspace:^ version: link:packages/config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@vscode/vsce': specifier: 3.9.2 version: 3.9.2 @@ -45,7 +45,7 @@ importers: version: 9.1.7 knip: specifier: 5.60.2 - version: 5.60.2(@types/node@20.19.43)(typescript@5.9.3) + version: 5.60.2(@types/node@22.20.1)(typescript@5.9.3) lint-staged: specifier: 16.4.0 version: 16.4.0 @@ -126,8 +126,8 @@ importers: specifier: ^11.18.0 version: 11.18.0(typescript@5.9.3) '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/react': specifier: 18.3.31 version: 18.3.31 @@ -148,7 +148,7 @@ importers: version: 4.22.4 vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) apps/vscode-e2e: devDependencies: @@ -168,8 +168,8 @@ importers: specifier: 10.0.10 version: 10.0.10 '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/vscode': specifier: 1.100.0 version: 1.100.0 @@ -208,11 +208,11 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/cloud: dependencies: @@ -233,8 +233,8 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/vscode': specifier: 1.100.0 version: 1.100.0 @@ -246,7 +246,7 @@ importers: version: 16.3.0 vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/config-eslint: devDependencies: @@ -311,14 +311,14 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(vitest@4.1.9) vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/ipc: dependencies: @@ -336,8 +336,8 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/node-ipc': specifier: 9.2.3 version: 9.2.3 @@ -361,8 +361,8 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/vscode': specifier: 1.100.0 version: 1.100.0 @@ -371,7 +371,7 @@ importers: version: 4.1.9(vitest@4.1.9) vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/types: dependencies: @@ -389,8 +389,8 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 ajv: specifier: 8.20.0 version: 8.20.0 @@ -402,7 +402,7 @@ importers: version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) zod-to-json-schema: specifier: 3.25.2 version: 3.25.2(zod@3.25.76) @@ -416,11 +416,11 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) src: dependencies: @@ -654,8 +654,8 @@ importers: specifier: 4.0.9 version: 4.0.9 '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/node-cache': specifier: 4.2.5 version: 4.2.5 @@ -703,7 +703,7 @@ importers: version: 6.0.1 vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) webview-ui: dependencies: @@ -757,7 +757,7 @@ importers: version: link:../packages/types '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.3.2(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.3.2(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@tanstack/react-query': specifier: ^5.68.0 version: 5.101.2(react@18.3.1) @@ -928,8 +928,8 @@ importers: specifier: 0.16.7 version: 0.16.7 '@types/node': - specifier: 20.19.43 - version: 20.19.43 + specifier: 22.20.1 + version: 22.20.1 '@types/react': specifier: 18.3.31 version: 18.3.31 @@ -947,7 +947,7 @@ importers: version: 1.57.5 '@vitejs/plugin-react': specifier: 5.2.0 - version: 5.2.0(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 5.2.0(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(vitest@4.1.9) @@ -962,10 +962,10 @@ importers: version: 26.1.0 vite: specifier: 8.1.0 - version: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + version: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) vitest: specifier: 4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -3165,8 +3165,8 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@20.19.43': - resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -8943,7 +8943,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@20.19.43)': + '@changesets/cli@2.31.0(@types/node@22.20.1)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -8959,7 +8959,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.2(@types/node@20.19.43) + '@inquirer/external-editor': 1.0.2(@types/node@22.20.1) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -9061,7 +9061,7 @@ snapshots: '@copilotkit/aimock@1.35.0(vitest@4.1.9)': optionalDependencies: - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@csstools/color-helpers@5.0.2': {} @@ -9322,12 +9322,12 @@ snapshots: figures: 6.1.0 ink: 6.6.0(@types/react@18.3.31)(react@19.2.7) - '@inquirer/external-editor@1.0.2(@types/node@20.19.43)': + '@inquirer/external-editor@1.0.2(@types/node@22.20.1)': dependencies: chardet: 2.1.0 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 20.19.43 + '@types/node': 22.20.1 '@isaacs/cliui@8.0.2': dependencies: @@ -9351,7 +9351,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.43 + '@types/node': 22.20.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -10487,12 +10487,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 - '@tailwindcss/vite@4.3.2(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.2(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.2 '@tailwindcss/oxide': 4.3.2 tailwindcss: 4.3.2 - vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@tanstack/query-core@5.101.2': {} @@ -10813,13 +10813,13 @@ snapshots: '@types/node-ipc@9.2.3': dependencies: - '@types/node': 20.19.43 + '@types/node': 22.20.1 '@types/node@12.20.55': {} '@types/node@14.18.63': {} - '@types/node@20.19.43': + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 @@ -10969,7 +10969,7 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -10977,7 +10977,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -10993,7 +10993,7 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/expect@4.1.9': dependencies: @@ -11004,13 +11004,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@4.1.9': dependencies: @@ -11039,7 +11039,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/utils@4.1.9': dependencies: @@ -13558,7 +13558,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.19.43 + '@types/node': 22.20.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -13732,10 +13732,10 @@ snapshots: kind-of@6.0.3: {} - knip@5.60.2(@types/node@20.19.43)(typescript@5.9.3): + knip@5.60.2(@types/node@22.20.1)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 20.19.43 + '@types/node': 22.20.1 fast-glob: 3.3.3 formatly: 0.2.4 jiti: 2.7.0 @@ -15083,7 +15083,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 - '@types/node': 20.19.43 + '@types/node': 22.20.1 long: 5.3.2 proxy-addr@2.0.7: @@ -16578,7 +16578,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -16586,17 +16586,17 @@ snapshots: rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 20.19.43 + '@types/node': 22.20.1 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 tsx: 4.22.4 yaml: 2.9.0 - vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -16613,11 +16613,11 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.43 + '@types/node': 22.20.1 '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) '@vitest/ui': 4.1.9(vitest@4.1.9) jsdom: 26.1.0 diff --git a/renovate.json b/renovate.json index 69ba659080..cffcfa1f0c 100644 --- a/renovate.json +++ b/renovate.json @@ -83,6 +83,11 @@ "matchUpdateTypes": ["patch"], "automerge": true }, + { + "description": "Keep @types/node pinned to the Node 22 LTS major.", + "matchPackageNames": ["@types/node"], + "allowedVersions": "22.x" + }, { "description": "Update extension-host VS Code types only with an intentional minimum-version review.", "matchPackageNames": ["@types/vscode"], diff --git a/src/package.json b/src/package.json index 31b6c31bca..2b022d9bd3 100644 --- a/src/package.json +++ b/src/package.json @@ -11,7 +11,7 @@ }, "engines": { "vscode": "^1.100.0", - "node": "20.20.2" + "node": "22.23.1" }, "author": { "name": "Zoo Code" @@ -535,7 +535,7 @@ "@types/clone-deep": "4.0.4", "@types/diff": "5.2.3", "@types/lodash.debounce": "4.0.9", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/node-cache": "4.2.5", "@types/proper-lockfile": "4.1.4", "@types/ps-tree": "1.1.6", diff --git a/webview-ui/package.json b/webview-ui/package.json index 83c1a84f90..c947f5e005 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -91,7 +91,7 @@ "@types/diff": "5.2.3", "@types/jest": "29.5.14", "@types/katex": "0.16.7", - "@types/node": "20.19.43", + "@types/node": "22.20.1", "@types/react": "18.3.31", "@types/react-dom": "18.3.7", "@types/shell-quote": "1.7.5", From dfb8d696323c5daea2d530e4617114c9fb27f534 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:13:21 -0400 Subject: [PATCH 61/89] fix(architect): instruct plans directory as workspace-relative ./plans (#968) The Architect mode custom instructions ended with 'put it in the /plans directory', which models interpreted as an absolute filesystem path, causing 'EROFS: read-only file system, mkdir /plans' when saving plan files. Reword to './plans' and clarify it is relative to the workspace root. Fixes #965 Co-authored-by: Toray Altas <6816042+taltas@users.noreply.github.com> --- packages/types/src/mode.ts | 2 +- .../add-custom-instructions/architect-mode-prompt.snap | 2 +- .../__snapshots__/add-custom-instructions/no-mcp-servers.snap | 2 +- .../__snapshots__/system-prompt/consistent-system-prompt.snap | 2 +- .../__snapshots__/system-prompt/with-computer-use-support.snap | 2 +- .../__snapshots__/system-prompt/with-diff-enabled-false.snap | 2 +- .../__snapshots__/system-prompt/with-diff-enabled-true.snap | 2 +- .../system-prompt/with-diff-enabled-undefined.snap | 2 +- .../system-prompt/with-different-viewport-size.snap | 2 +- .../__snapshots__/system-prompt/with-mcp-hub-provided.snap | 2 +- .../__snapshots__/system-prompt/with-undefined-mcp-hub.snap | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 49a98a3dc1..ebbfb5bf38 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -182,7 +182,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ description: "Plan and design before implementation", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "mcp"], customInstructions: - "1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**\n\n**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**\n\nUnless told otherwise, if you want to save a plan file, put it in the /plans directory", + '1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you\'ve gained more context about the user\'s request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes ("") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**\n\n**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**\n\nUnless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans)', }, { slug: "code", diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index 0b443b201c..d6fd17ba2f 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap index 0b443b201c..d6fd17ba2f 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/no-mcp-servers.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index cb00166d35..2a1533bfef 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index cb00166d35..2a1533bfef 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index 89b1231ad9..f633cbfcf2 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -130,7 +130,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 89b1231ad9..f633cbfcf2 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -130,7 +130,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index 89b1231ad9..f633cbfcf2 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -130,7 +130,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index cb00166d35..2a1533bfef 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index 2d2f3701ca..5660cd4def 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -120,7 +120,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index cb00166d35..2a1533bfef 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -118,7 +118,7 @@ Mode-specific Instructions: **CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.** -Unless told otherwise, if you want to save a plan file, put it in the /plans directory +Unless told otherwise, if you want to save a plan file, put it in the ./plans directory (a directory named "plans" relative to the workspace root, not the absolute filesystem path /plans) Rules: # Rules from .clinerules-architect: From ff17dcb67cf7d82ba0865db1ad54402ceb1b6c3b Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:03:06 -0400 Subject: [PATCH 62/89] feat(TaskRegistry): introduce TaskRegistry and migrate clineStack (#1014) * feat(TaskRegistry): introduce TaskRegistry and migrate clineStack * refactor(tests): replace `as any` casts with bracket notation for private member access * chore(eslint): enforce no-explicit-any with bulk suppressions for existing violations * docs(TaskRegistry): push method * fixup! chore(eslint): enforce no-explicit-any with bulk suppressions for existing violations --- packages/config-eslint/base.js | 7 - packages/config-eslint/package.json | 1 - pnpm-lock.yaml | 8 - src/__tests__/helpers/provider-stub.ts | 46 +- .../removeClineFromStack-delegation.spec.ts | 166 +- src/__tests__/single-open-invariant.spec.ts | 64 +- src/core/task/TaskRegistry.ts | 119 ++ src/core/task/__tests__/TaskRegistry.spec.ts | 301 +++ src/core/webview/ClineProvider.ts | 86 +- .../ClineProvider.flicker-free-cancel.spec.ts | 165 +- .../webview/__tests__/ClineProvider.spec.ts | 3 +- src/eslint-suppressions.json | 1822 +++++++++++++++++ src/eslint.config.mjs | 3 +- 13 files changed, 2596 insertions(+), 195 deletions(-) create mode 100644 src/core/task/TaskRegistry.ts create mode 100644 src/core/task/__tests__/TaskRegistry.spec.ts create mode 100644 src/eslint-suppressions.json diff --git a/packages/config-eslint/base.js b/packages/config-eslint/base.js index 6a15f09c03..6f579e2f97 100644 --- a/packages/config-eslint/base.js +++ b/packages/config-eslint/base.js @@ -2,8 +2,6 @@ import js from "@eslint/js" import eslintConfigPrettier from "eslint-config-prettier" import turboPlugin from "eslint-plugin-turbo" import tseslint from "typescript-eslint" -import onlyWarn from "eslint-plugin-only-warn" - /** * A shared ESLint configuration for the repository. * @@ -21,11 +19,6 @@ export const config = [ "turbo/no-undeclared-env-vars": "off", }, }, - { - plugins: { - onlyWarn, - }, - }, { ignores: ["dist/**"], }, diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index 8c10849f33..8c01525543 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -12,7 +12,6 @@ "@next/eslint-plugin-next": "15.5.20", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", - "eslint-plugin-only-warn": "1.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-turbo": "2.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7513d35d27..ea26770f32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,9 +262,6 @@ importers: eslint-config-prettier: specifier: 10.1.8 version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-only-warn: - specifier: 1.2.1 - version: 1.2.1 eslint-plugin-react: specifier: 7.37.5 version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -4599,9 +4596,6 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-only-warn@1.2.1: - resolution: {integrity: sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w==} - eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -12424,8 +12418,6 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-only-warn@1.2.1: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) diff --git a/src/__tests__/helpers/provider-stub.ts b/src/__tests__/helpers/provider-stub.ts index c91016d93a..59dde33933 100644 --- a/src/__tests__/helpers/provider-stub.ts +++ b/src/__tests__/helpers/provider-stub.ts @@ -1,21 +1,55 @@ import { ClineProvider } from "../../core/webview/ClineProvider" +import { TaskRegistry } from "../../core/task/TaskRegistry" +import { type Task } from "../../core/task/Task" + +type ProviderStubFields = { + delegationTransitionLocks?: Map> + cancelledDelegationChildIds?: Set + log?: ReturnType + taskHistoryStore?: { get: (id: string) => unknown } + taskRegistry?: TaskRegistry + clineStack?: Task[] + tasks?: Task[] + runDelegationTransition?: unknown + removeClineFromStack?: unknown + evictCurrentTask?: unknown +} + +type PrivateProviderMethods = { + runDelegationTransition: (this: unknown, ...args: unknown[]) => unknown + removeClineFromStack: (this: unknown, ...args: unknown[]) => unknown + evictCurrentTask: (this: unknown, ...args: unknown[]) => unknown +} /** * Augments a plain stub object with the instance fields and bound methods that * ClineProvider methods read from `this` (runDelegationTransition, * delegationTransitionLocks, cancelledDelegationChildIds, cancellingDelegationChildIds), - * so tests can call private methods via `(ClineProvider.prototype as any).method.call(stub, …)` + * so tests can call private ClineProvider methods against a plain object * without instantiating a real ClineProvider. + * + * Pass `tasks` (array of Task mocks) to pre-seed the registry in stack order. + * The legacy `clineStack` key is accepted and converted automatically. */ -export function makeProviderStub(stub: T): T { - const s = stub as any - const proto = ClineProvider.prototype as any +export function makeProviderStub(stub: T): ClineProvider { + const s = stub as T & ProviderStubFields + const proto = ClineProvider.prototype as unknown as PrivateProviderMethods s.delegationTransitionLocks ??= new Map() s.cancelledDelegationChildIds ??= new Set() s.log ??= vi.fn() s.taskHistoryStore ??= { get: () => undefined } - s.runDelegationTransition = proto.runDelegationTransition.bind(s) + + // Convert legacy clineStack array into a TaskRegistry + if (!s.taskRegistry) { + const registry = new TaskRegistry() + const seed: Task[] = s.clineStack ?? s.tasks ?? [] + for (const t of seed) registry.push(t) + s.taskRegistry = registry + } + delete s.clineStack + + s.runDelegationTransition ??= proto.runDelegationTransition.bind(s) s.removeClineFromStack ??= proto.removeClineFromStack.bind(s) s.evictCurrentTask ??= proto.evictCurrentTask.bind(s) - return s + return s as unknown as ClineProvider } diff --git a/src/__tests__/removeClineFromStack-delegation.spec.ts b/src/__tests__/removeClineFromStack-delegation.spec.ts index a91900c38a..e211a8f4ab 100644 --- a/src/__tests__/removeClineFromStack-delegation.spec.ts +++ b/src/__tests__/removeClineFromStack-delegation.spec.ts @@ -1,10 +1,29 @@ // npx vitest run __tests__/removeClineFromStack-delegation.spec.ts -import { describe, it, expect, vi } from "vitest" +import { describe, it, expect, vi, type MockedFunction } from "vitest" import { ClineProvider } from "../core/webview/ClineProvider" +import { TaskRegistry } from "../core/task/TaskRegistry" +import { type Task } from "../core/task/Task" import { makeProviderStub } from "./helpers/provider-stub" -// After the refactor: removeClineFromStack() is pure lifecycle — it pops, aborts, and +type MockTask = Pick & + Partial> & { + emit: ReturnType + abortTask: ReturnType + } + +type PrivateClineProviderMethods = { + removeClineFromStack: (this: ClineProvider) => ReturnType + markDelegatedChildInterrupted: ( + this: ClineProvider, + ...args: Parameters + ) => ReturnType + evictCurrentTask: (this: ClineProvider) => ReturnType +} + +const privateClineProvider = ClineProvider.prototype as unknown as PrivateClineProviderMethods + +// After the refactor: removeClineFromStack() is pure lifecycle — it removes the focused task, aborts, and // cleans up listeners. It does NOT mutate delegation metadata. All delegated→active // transitions are owned by reopenParentFromDelegation() (normal child completion) or // markDelegatedChildInterrupted() (live eviction via navigation / new-task / clear). @@ -12,7 +31,7 @@ import { makeProviderStub } from "./helpers/provider-stub" function buildMockProvider(opts: { childTaskId: string parentTaskId?: string - parentHistoryItem?: Record + parentHistoryItem?: Record childStatus?: string }) { const childTask = { @@ -31,13 +50,13 @@ function buildMockProvider(opts: { throw new Error("Task not found") }) - const taskHistoryStoreData: Record = {} + const taskHistoryStoreData: Record = {} if (opts.childStatus) { taskHistoryStoreData[opts.childTaskId] = { status: opts.childStatus } } const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -49,17 +68,47 @@ function buildMockProvider(opts: { } describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation side effects", () => { - it("pops the task, aborts it, and clears listeners", async () => { + it("removes the focused task, aborts it, and clears listeners", async () => { const { provider, childTask } = buildMockProvider({ childTaskId: "child-1" }) - expect(provider.clineStack).toHaveLength(1) + expect(provider["taskRegistry"].length).toBe(1) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(childTask.abortTask).toHaveBeenCalledWith(true) expect(childTask.emit).toHaveBeenCalledWith(expect.stringContaining("taskUnfocused")) }) + it("removes the focused task even when it is not the top stack entry", async () => { + const focusedTask = { + taskId: "focused-1", + instanceId: "focused-inst", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + const topTask = { + taskId: "top-1", + instanceId: "top-inst", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + const provider = makeProviderStub({ + tasks: [focusedTask, topTask] as unknown as Task[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId: vi.fn(), + updateTaskHistory: vi.fn(), + }) + provider["taskRegistry"].setCurrent("focused-1") + + await privateClineProvider.removeClineFromStack.call(provider) + + expect(provider["taskRegistry"].taskIds).toEqual(["top-1"]) + expect(provider["taskRegistry"].current).toBe(topTask) + expect(focusedTask.abortTask).toHaveBeenCalledWith(true) + expect(topTask.abortTask).not.toHaveBeenCalled() + }) + it("does NOT mutate parent metadata when a delegated child is popped (repair removed)", async () => { const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ childTaskId: "child-1", @@ -72,9 +121,9 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation }, }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) // Navigation/disposal must never silently flip the parent to active expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() @@ -92,9 +141,9 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation childStatus: "interrupted", }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) @@ -104,26 +153,26 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation childTaskId: "standalone-1", }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) it("handles empty stack gracefully", async () => { const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId: vi.fn(), updateTaskHistory: vi.fn(), }) - await expect((ClineProvider.prototype as any).removeClineFromStack.call(provider)).resolves.not.toThrow() + await expect(privateClineProvider.removeClineFromStack.call(provider)).resolves.not.toThrow() - expect((provider as any).getTaskWithId).not.toHaveBeenCalled() - expect((provider as any).updateTaskHistory).not.toHaveBeenCalled() + expect(provider["getTaskWithId"]).not.toHaveBeenCalled() + expect(provider["updateTaskHistory"]).not.toHaveBeenCalled() }) }) @@ -159,7 +208,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const postMessageToWebview = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -170,7 +219,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -203,7 +252,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const updateTaskHistory = vi.fn().mockResolvedValue([]) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId: vi.fn(), @@ -213,7 +262,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -235,7 +284,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -245,7 +294,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -270,8 +319,11 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", return { historyItem: { id: childTaskId, status: "interrupted", parentTaskId } } }) + const realRunDelegation = ClineProvider.prototype["runDelegationTransition"].bind({ + delegationTransitionLocks: new Map(), + }) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -289,16 +341,14 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", return undefined }), }, + // Wrap the real runDelegationTransition to set lockAcquired before the lock fires + runDelegationTransition: async (parentId: string, fn: () => Promise) => { + lockAcquired = true + return realRunDelegation(parentId, fn) + }, }) - // Patch runDelegationTransition to set lockAcquired before calling fn - const realRunDelegation = (provider as any).runDelegationTransition.bind(provider) - ;(provider as any).runDelegationTransition = async (_parentId: string, fn: () => Promise) => { - lockAcquired = true - return realRunDelegation(_parentId, fn) - } - - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -315,7 +365,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const getTaskWithId = vi.fn().mockRejectedValue(new Error("store unavailable")) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log, getTaskWithId, @@ -326,7 +376,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }) await expect( - (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }), @@ -374,7 +424,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -393,7 +443,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation // Simulate the navigation logic from createTaskWithHistoryItem: // when the target is a delegated parent and current task is its interrupted child, // removeClineFromStack must NOT repair parent to active. - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) // Parent must stay delegated — no write at all expect(updateTaskHistory).not.toHaveBeenCalledWith(expect.objectContaining({ id: parentTaskId })) @@ -428,7 +478,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation const postMessageToWebview = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -444,7 +494,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -480,7 +530,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { get: vi.fn((id: string) => (id === childTaskId ? childHistoryItem : undefined)) }, @@ -488,9 +538,9 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(markDelegatedChildInterrupted).toHaveBeenCalledWith({ childTaskId, parentTaskId }) }) @@ -498,7 +548,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn() const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => undefined), taskHistoryStore: { get: vi.fn(() => undefined) }, @@ -506,7 +556,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() }) @@ -522,7 +572,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn() const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { @@ -532,7 +582,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() }) @@ -552,7 +602,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn().mockRejectedValue(new Error("lock contention")) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { @@ -562,14 +612,14 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await expect((ClineProvider.prototype as any).evictCurrentTask.call(provider)).rejects.toThrow( - "lock contention", - ) + await expect(privateClineProvider.evictCurrentTask.call(provider)).rejects.toThrow("lock contention") }) }) describe("onTaskCompleted callback — writes completed status before re-emitting", () => { - function buildCallbackProvider(taskHistoryStoreGet: (id: string) => any) { + type CallbackHistoryItem = { id: string; status?: string; [key: string]: unknown } + + function buildCallbackProvider(taskHistoryStoreGet: (id: string) => CallbackHistoryItem | undefined) { const updateTaskHistory = vi.fn().mockResolvedValue([]) const emit = vi.fn() const log = vi.fn() @@ -584,7 +634,7 @@ describe("onTaskCompleted callback — writes completed status before re-emittin listeners[event] = listeners[event] || [] listeners[event].push(fn) }), - emit: vi.fn((event: string, ...args: any[]) => { + emit: vi.fn((event: string, ...args: unknown[]) => { listeners[event]?.forEach((fn) => fn(...args)) }), } @@ -602,16 +652,16 @@ describe("onTaskCompleted callback — writes completed status before re-emittin // The real callback is set in the constructor body; we replicate the relevant portion. const onTaskCompleted = async (taskId: string) => { try { - const existing = (provider as any).taskHistoryStore.get(taskId) + const existing = provider["taskHistoryStore"].get(taskId) if (existing && existing.status !== "completed") { - await (provider as any).updateTaskHistory({ ...existing, status: "completed" }) + await provider["updateTaskHistory"]({ ...existing, status: "completed" }) } } catch (err) { - ;(provider as any).log( + provider["log"]( `[onTaskCompleted] Failed to write completed status for ${taskId}: ${err instanceof Error ? err.message : String(err)}`, ) } - ;(provider as any).emit("TaskCompleted", taskId, {}, {}) + emit("TaskCompleted", taskId, {}, {}) } return { onTaskCompleted, updateTaskHistory, emit, log } @@ -660,7 +710,7 @@ describe("onTaskCompleted callback — writes completed status before re-emittin const { onTaskCompleted, updateTaskHistory, log } = buildCallbackProvider((id) => id === "task-1" ? existingItem : undefined, ) - ;(updateTaskHistory as any).mockRejectedValue(new Error("disk full")) + vi.mocked(updateTaskHistory).mockRejectedValue(new Error("disk full")) await expect(onTaskCompleted("task-1")).resolves.not.toThrow() diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 29aab5d15d..0cb9425680 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -1,19 +1,44 @@ // npx vitest run __tests__/single-open-invariant.spec.ts import { describe, it, expect, vi, beforeEach } from "vitest" +import { type OutputChannel } from "vscode" import { ClineProvider } from "../core/webview/ClineProvider" +import { TaskRegistry } from "../core/task/TaskRegistry" +import { type Task } from "../core/task/Task" import { API } from "../extension/api" import * as ProfileValidatorMod from "../shared/ProfileValidator" +type PrivateClineProviderMethods = { + createTask: ( + this: unknown, + text?: string, + images?: string[], + parentTask?: Task, + options?: Parameters[3], + ) => ReturnType + createTaskWithHistoryItem: ( + this: unknown, + ...args: Parameters + ) => ReturnType + evictCurrentTask: (this: unknown) => ReturnType +} + +const privateClineProvider = ClineProvider.prototype as unknown as PrivateClineProviderMethods + // Mock Task class used by ClineProvider to avoid heavy startup vi.mock("../core/task/Task", () => { class TaskStub { public taskId: string public instanceId = "inst" - public parentTask?: any - public apiConfiguration: any - public rootTask?: any - constructor(opts: any) { + public parentTask?: unknown + public apiConfiguration: unknown + public rootTask?: unknown + constructor(opts: { + historyItem?: { id: string } + parentTask?: unknown + apiConfiguration?: unknown + onCreated?: (t: TaskStub) => void + }) { this.taskId = opts.historyItem?.id ?? `task-${Math.random().toString(36).slice(2, 8)}` this.parentTask = opts.parentTask this.apiConfiguration = opts.apiConfiguration ?? { apiProvider: "anthropic" } @@ -39,15 +64,16 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) - const existingTask = { taskId: "existing-1" } + const existingTask = { taskId: "existing-1", abort: false, abandoned: false } + const registry = new TaskRegistry() + registry.push(existingTask as unknown as Task) const provider = { - // Simulate an existing task present in stack - clineStack: [existingTask], + taskRegistry: registry, getCurrentTask: vi.fn(() => existingTask), taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ @@ -74,7 +100,7 @@ describe("Single-open-task invariant", () => { }, } as unknown as ClineProvider - await (ClineProvider.prototype as any).createTask.call(provider, "New task") + await privateClineProvider.createTask.call(provider, "New task") expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -85,10 +111,12 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) - const parentTask = { taskId: "parent-1" } + const parentTask = { taskId: "parent-1", abort: false, abandoned: false } + const registry2 = new TaskRegistry() + registry2.push(parentTask as unknown as Task) const provider = { - clineStack: [parentTask], + taskRegistry: registry2, setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, @@ -114,7 +142,7 @@ describe("Single-open-task invariant", () => { }, } as unknown as ClineProvider - await (ClineProvider.prototype as any).createTask.call(provider, "Subtask", undefined, parentTask as any) + await privateClineProvider.createTask.call(provider, "Subtask", undefined, parentTask as unknown as Task) expect(removeClineFromStack).not.toHaveBeenCalled() expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -130,7 +158,7 @@ describe("Single-open-task invariant", () => { taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, removeClineFromStack, addClineToStack, @@ -174,7 +202,7 @@ describe("Single-open-task invariant", () => { workspace: "/tmp", } - const task = await (ClineProvider.prototype as any).createTaskWithHistoryItem.call(provider, historyItem) + const task = await privateClineProvider.createTaskWithHistoryItem.call(provider, historyItem) expect(task).toBeTruthy() expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -184,12 +212,12 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTask = vi.fn().mockResolvedValue({ taskId: "ipc-1" }) const provider = { - context: {} as any, + context: {} as unknown, getCurrentTask: vi.fn(() => undefined), taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, removeClineFromStack, postStateToWebview: vi.fn(), @@ -197,7 +225,7 @@ describe("Single-open-task invariant", () => { createTask, getValues: vi.fn(() => ({})), providerSettingsManager: { saveConfig: vi.fn() }, - on: vi.fn((ev: any, cb: any) => { + on: vi.fn((ev: unknown, cb: unknown) => { if (ev === "taskCreated") { // no-op for this test } @@ -205,7 +233,7 @@ describe("Single-open-task invariant", () => { }), } as unknown as ClineProvider - const output = { appendLine: vi.fn() } as any + const output = { appendLine: vi.fn() } as unknown as OutputChannel const api = new API(output, provider, undefined, false) const taskId = await api.startNewTask({ diff --git a/src/core/task/TaskRegistry.ts b/src/core/task/TaskRegistry.ts new file mode 100644 index 0000000000..728870683d --- /dev/null +++ b/src/core/task/TaskRegistry.ts @@ -0,0 +1,119 @@ +import { type Task } from "./Task" + +/** + * Expand-Contract adapter replacing `clineStack: Task[]` in ClineProvider. + * + * Phase A+B: adapter is live, all 23 call sites migrated, concurrent access + * methods available. Maintains a LIFO stack of task IDs alongside a Map for + * O(1) lookup. Invariant: tasks.size === stack.length at all times. + * + * Phase C (future): remove internal stack once all callers use map-based access. + */ +export class TaskRegistry { + private tasks = new Map() + private stack: string[] = [] + private _currentTaskId: string | undefined + + /** + * Add a task and focus it. If the task ID already exists, remove the + * existing entry first so this behaves as "move to top" instead of creating + * duplicate stack entries. + */ + push(task: Task): void { + if (this.tasks.has(task.taskId)) { + this.remove(task.taskId) + } + this.tasks.set(task.taskId, task) + this.stack.push(task.taskId) + this._currentTaskId = task.taskId + } + + pop(): Task | undefined { + const id = this.stack.pop() + if (id === undefined) return undefined + const task = this.tasks.get(id) + this.tasks.delete(id) + if (this._currentTaskId === id) { + this._currentTaskId = this.stack[this.stack.length - 1] + } + return task + } + + get current(): Task | undefined { + return this._currentTaskId !== undefined ? this.tasks.get(this._currentTaskId) : undefined + } + + /** + * Switch UI focus to a different task without mutating the stack order. + * Throws if the taskId is not in the registry. + */ + setCurrent(taskId: string): void { + if (!this.tasks.has(taskId)) { + throw new Error(`[TaskRegistry] setCurrent: unknown taskId ${taskId}`) + } + this._currentTaskId = taskId + } + + getById(id: string): Task | undefined { + return this.tasks.get(id) + } + + getAll(): Task[] { + return this.stack.map((id) => this.tasks.get(id)!) + } + + /** All tasks that are not aborted or abandoned. */ + getRunning(): Task[] { + return this.getAll().filter((t) => !t.abort && !t.abandoned) + } + + /** True only if the task is in the registry and is not aborted or abandoned. */ + hasRunning(taskId: string): boolean { + const t = this.tasks.get(taskId) + return t !== undefined && !t.abort && !t.abandoned + } + + /** Remove a task by ID regardless of its stack position. */ + remove(taskId: string): Task | undefined { + const task = this.tasks.get(taskId) + if (task === undefined) return undefined + this.tasks.delete(taskId) + const idx = this.stack.indexOf(taskId) + if (idx !== -1) this.stack.splice(idx, 1) + if (this._currentTaskId === taskId) { + this._currentTaskId = this.stack[this.stack.length - 1] + } + return task + } + + /** + * Replace a task in-place: same stack index, same current pointer. + * Used by rehydration so focus and stack order are both preserved. + * Throws if taskId is not in the registry. + */ + replace(taskId: string, replacement: Task): Task { + const idx = this.stack.indexOf(taskId) + if (idx === -1) { + throw new Error(`[TaskRegistry] replace: unknown taskId ${taskId}`) + } + if (replacement.taskId !== taskId && this.tasks.has(replacement.taskId)) { + throw new Error(`[TaskRegistry] replace: duplicate taskId ${replacement.taskId}`) + } + const old = this.tasks.get(taskId)! + this.tasks.delete(taskId) + this.tasks.set(replacement.taskId, replacement) + this.stack[idx] = replacement.taskId + if (this._currentTaskId === taskId) { + this._currentTaskId = replacement.taskId + } + return old + } + + get length(): number { + return this.stack.length + } + + get taskIds(): string[] { + return [...this.stack] + } +} diff --git a/src/core/task/__tests__/TaskRegistry.spec.ts b/src/core/task/__tests__/TaskRegistry.spec.ts new file mode 100644 index 0000000000..4493d83277 --- /dev/null +++ b/src/core/task/__tests__/TaskRegistry.spec.ts @@ -0,0 +1,301 @@ +import { TaskRegistry } from "../TaskRegistry" +import { type Task } from "../Task" + +function makeTask(taskId: string, opts: { abort?: boolean; abandoned?: boolean } = {}): Task { + return { taskId, abort: opts.abort ?? false, abandoned: opts.abandoned ?? false } as unknown as Task +} + +function assertInvariant(registry: TaskRegistry) { + const ids = registry.taskIds + expect(ids.length).toBe(registry.length) + for (const id of ids) { + expect(registry.getById(id)).toBeDefined() + } + if (registry.current !== undefined) { + expect(registry.getById(registry.current.taskId)).toBe(registry.current) + } +} + +describe("TaskRegistry", () => { + describe("push / pop", () => { + it("push adds to registry and makes task current", () => { + const r = new TaskRegistry() + const t = makeTask("a") + r.push(t) + expect(r.length).toBe(1) + expect(r.current).toBe(t) + expect(r.getById("a")).toBe(t) + assertInvariant(r) + }) + + it("push of second task makes it current", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(r.current).toBe(t2) + expect(r.length).toBe(2) + assertInvariant(r) + }) + + it("push replaces duplicate task ids", () => { + const r = new TaskRegistry() + const first = makeTask("a") + const second = makeTask("a") + r.push(first) + r.push(second) + expect(r.taskIds).toEqual(["a"]) + expect(r.length).toBe(1) + expect(r.current).toBe(second) + expect(r.getById("a")).toBe(second) + assertInvariant(r) + }) + + it("pop removes top task and restores previous current", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + const popped = r.pop() + expect(popped).toBe(t2) + expect(r.length).toBe(1) + expect(r.current).toBe(t1) + expect(r.getById("b")).toBeUndefined() + assertInvariant(r) + }) + + it("pop on empty registry returns undefined", () => { + const r = new TaskRegistry() + expect(r.pop()).toBeUndefined() + expect(r.length).toBe(0) + expect(r.current).toBeUndefined() + }) + + it("pop of last task leaves current undefined", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.pop() + expect(r.current).toBeUndefined() + expect(r.length).toBe(0) + assertInvariant(r) + }) + + it("pop of non-current top does not change focus", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + r.push(t1) + r.push(t2) + r.push(t3) + r.setCurrent("a") + const popped = r.pop() + expect(popped).toBe(t3) + expect(r.current).toBe(t1) + expect(r.taskIds).toEqual(["a", "b"]) + assertInvariant(r) + }) + }) + + describe("getById", () => { + it("returns correct task by id", () => { + const r = new TaskRegistry() + const t = makeTask("x") + r.push(t) + expect(r.getById("x")).toBe(t) + }) + + it("returns undefined for unknown id", () => { + const r = new TaskRegistry() + expect(r.getById("nope")).toBeUndefined() + }) + }) + + describe("remove", () => { + it("removes task by id regardless of position", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + r.push(t1) + r.push(t2) + r.push(t3) + const removed = r.remove("b") + expect(removed).toBe(t2) + expect(r.length).toBe(2) + expect(r.getById("b")).toBeUndefined() + expect(r.taskIds).toEqual(["a", "c"]) + assertInvariant(r) + }) + + it("remove of current task updates current to new top", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + r.remove("b") + expect(r.current).toBe(t1) + assertInvariant(r) + }) + + it("returns undefined for unknown id", () => { + const r = new TaskRegistry() + expect(r.remove("nope")).toBeUndefined() + }) + }) + + describe("replace", () => { + it("preserves stack index when replacing the current task", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + const t2v2 = makeTask("b-v2") + r.push(t1) + r.push(t2) + r.push(t3) + r.setCurrent("b") + r.replace("b", t2v2) + expect(r.taskIds).toEqual(["a", "b-v2", "c"]) + expect(r.current).toBe(t2v2) + expect(r.getById("b")).toBeUndefined() + expect(r.getById("b-v2")).toBe(t2v2) + assertInvariant(r) + }) + + it("preserves stack index when replacing a non-current task", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t1v2 = makeTask("a-v2") + r.push(t1) + r.push(t2) + r.replace("a", t1v2) + expect(r.taskIds).toEqual(["a-v2", "b"]) + expect(r.current).toBe(t2) + assertInvariant(r) + }) + + it("returns the old task", () => { + const r = new TaskRegistry() + const t = makeTask("a") + const t2 = makeTask("a-v2") + r.push(t) + expect(r.replace("a", t2)).toBe(t) + }) + + it("throws for unknown taskId", () => { + const r = new TaskRegistry() + expect(() => r.replace("nope", makeTask("x"))).toThrow(/unknown taskId/) + }) + + it("throws when replacement task id already exists elsewhere", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(() => r.replace("a", makeTask("b"))).toThrow(/duplicate taskId/) + expect(r.taskIds).toEqual(["a", "b"]) + expect(r.getById("a")).toBe(t1) + expect(r.getById("b")).toBe(t2) + assertInvariant(r) + }) + }) + + describe("setCurrent", () => { + it("changes current without mutating stack order", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + r.setCurrent("a") + expect(r.current).toBe(t1) + expect(r.taskIds).toEqual(["a", "b"]) + assertInvariant(r) + }) + + it("throws for unknown taskId", () => { + const r = new TaskRegistry() + expect(() => r.setCurrent("nope")).toThrow(/unknown taskId/) + }) + }) + + describe("getAll", () => { + it("returns tasks in stack order", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(r.getAll()).toEqual([t1, t2]) + }) + }) + + describe("getRunning / hasRunning", () => { + it("getRunning excludes aborted tasks", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.push(makeTask("b", { abort: true })) + r.push(makeTask("c")) + const running = r.getRunning() + expect(running.map((t) => t.taskId)).toEqual(["a", "c"]) + }) + + it("getRunning excludes abandoned tasks", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.push(makeTask("b", { abandoned: true })) + const running = r.getRunning() + expect(running.map((t) => t.taskId)).toEqual(["a"]) + }) + + it("hasRunning returns true for a live task", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + expect(r.hasRunning("a")).toBe(true) + }) + + it("hasRunning returns false for an aborted task", () => { + const r = new TaskRegistry() + r.push(makeTask("a", { abort: true })) + expect(r.hasRunning("a")).toBe(false) + }) + + it("hasRunning returns false for an unknown task", () => { + const r = new TaskRegistry() + expect(r.hasRunning("nope")).toBe(false) + }) + }) + + describe("taskIds / length", () => { + it("taskIds is a snapshot (not a live reference)", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + const ids = r.taskIds + r.push(makeTask("b")) + expect(ids).toEqual(["a"]) + }) + }) + + describe("dual-write invariant", () => { + it("holds after a sequence of mixed operations", () => { + const r = new TaskRegistry() + const tasks = (["a", "b", "c", "d"] as const).map((id) => makeTask(id)) + for (const t of tasks) { + r.push(t) + assertInvariant(r) + } + r.remove("b") + assertInvariant(r) + r.pop() + assertInvariant(r) + r.setCurrent("a") + assertInvariant(r) + }) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fcfd916d8e..643c4f5a22 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -52,6 +52,7 @@ import { isRetiredProvider, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" +import { TaskRegistry } from "../task/TaskRegistry" import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts" import { TelemetryService } from "@roo-code/telemetry" import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" @@ -164,7 +165,7 @@ export class ClineProvider private disposables: vscode.Disposable[] = [] private webviewDisposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel - private clineStack: Task[] = [] + private taskRegistry = new TaskRegistry() private delegationTransitionLocks?: Map> private cancelledDelegationChildIds = new Set() private codeIndexStatusSubscription?: vscode.Disposable @@ -454,14 +455,14 @@ export class ClineProvider this.log("Cloud profile synchronization is disabled in compatibility mode") } - // Adds a new Task instance to clineStack, marking the start of a new task. + // Adds a new Task instance to the registry, marking the start of a new task. // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the // previous task. async addClineToStack(task: Task) { // Add this cline instance into the stack that represents the order of // all the called tasks. - this.clineStack.push(task) + this.taskRegistry.push(task) task.emit(RooCodeEventName.TaskFocused) // Perform special setup provider specific tasks. @@ -496,12 +497,15 @@ export class ClineProvider // Removes and destroys the top Cline instance (the current finished task), // activating the previous one (resuming the parent task). async removeClineFromStack() { - if (this.clineStack.length === 0) { + if (this.taskRegistry.length === 0) { return } - // Pop the top Cline instance from the stack. - let task = this.clineStack.pop() + // Remove the focused Cline instance from the stack. + let task = this.taskRegistry.current + if (task) { + task = this.taskRegistry.remove(task.taskId) + } if (task) { task.emit(RooCodeEventName.TaskUnfocused) @@ -619,11 +623,11 @@ export class ClineProvider } getTaskStackSize(): number { - return this.clineStack.length + return this.taskRegistry.length } public getCurrentTaskStack(): string[] { - return this.clineStack.map((cline) => cline.taskId) + return this.taskRegistry.taskIds } // Pending Edit Operations Management @@ -681,10 +685,10 @@ export class ClineProvider // Clear all tasks from the stack. The first pop goes through evictCurrentTask() // so an active delegated child is marked interrupted before the extension shuts down, // rather than being left persisted as "active" across the reload. - if (this.clineStack.length > 0) { + if (this.taskRegistry.length > 0) { await this.evictCurrentTask() } - while (this.clineStack.length > 0) { + while (this.taskRegistry.length > 0) { await this.removeClineFromStack() } @@ -1186,29 +1190,29 @@ export class ClineProvider if (isRehydratingCurrentTask) { // Replace the current task in-place to avoid UI flicker - const stackIndex = this.clineStack.length - 1 + const oldTask = this.taskRegistry.current - // Properly dispose of the old task to ensure garbage collection - const oldTask = this.clineStack[stackIndex] + if (oldTask) { + // Abort the old task to stop running processes and mark as abandoned + try { + await oldTask.abortTask(true) + } catch (e) { + this.log( + `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`, + ) + } - // Abort the old task to stop running processes and mark as abandoned - try { - await oldTask.abortTask(true) - } catch (e) { - this.log( - `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`, - ) - } + // Remove event listeners from the old task + const cleanupFunctions = this.taskEventListeners.get(oldTask) + if (cleanupFunctions) { + cleanupFunctions.forEach((cleanup) => cleanup()) + this.taskEventListeners.delete(oldTask) + } - // Remove event listeners from the old task - const cleanupFunctions = this.taskEventListeners.get(oldTask) - if (cleanupFunctions) { - cleanupFunctions.forEach((cleanup) => cleanup()) - this.taskEventListeners.delete(oldTask) + // Replace in-place: preserves stack index and current pointer + this.taskRegistry.replace(oldTask.taskId, task) } - // Replace the task in the stack - this.clineStack[stackIndex] = task task.emit(RooCodeEventName.TaskFocused) // Perform preparation tasks and set up event listeners @@ -2029,13 +2033,7 @@ export class ClineProvider /* Condenses a task's message history to use fewer tokens. */ async condenseTaskContext(taskId: string) { - let task: Task | undefined - for (let i = this.clineStack.length - 1; i >= 0; i--) { - if (this.clineStack[i].taskId === taskId) { - task = this.clineStack[i] - break - } - } + const task = this.taskRegistry.getById(taskId) if (!task) { throw new Error(`Task with id ${taskId} not found in stack`) } @@ -3027,11 +3025,7 @@ export class ClineProvider */ public getCurrentTask(): Task | undefined { - if (this.clineStack.length === 0) { - return undefined - } - - return this.clineStack[this.clineStack.length - 1] + return this.taskRegistry.current } private logWebviewHiddenDiagnostics(): void { @@ -3043,7 +3037,7 @@ export class ClineProvider `[Zoo Code] Webview hidden during active task.\n` + ` taskId: ${task.taskId}\n` + ` messageCount: ${task.clineMessages.length}\n` + - ` stackDepth: ${this.clineStack.length}\n` + + ` stackDepth: ${this.taskRegistry.length}\n` + ` timestamp: ${new Date().toISOString()}\n` + `If the panel appears gray after this, share this log with support@zoocode.dev`, ) @@ -3176,12 +3170,12 @@ export class ClineProvider task: text, images, experiments, - rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, + rootTask: this.taskRegistry.getAll()[0], parentTask, - taskNumber: this.clineStack.length + 1, + taskNumber: this.taskRegistry.length + 1, onCreated: this.taskCreationCallback, initialTodos: options.initialTodos, - // Ensure this task is present in clineStack before startTask() emits + // Ensure this task is present in the registry before startTask() emits // its initial state update, so state.currentTaskId is available ASAP. startTask: false, diffFuzzyThreshold, @@ -3349,8 +3343,8 @@ export class ClineProvider // Clear the current task without treating it as a subtask. // This is used when the user cancels a task that is not a subtask. public async clearTask(): Promise { - if (this.clineStack.length > 0) { - const task = this.clineStack[this.clineStack.length - 1] + const task = this.taskRegistry.current + if (task) { console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`) await this.removeClineFromStack() } diff --git a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts index 37aae2ddb7..3513bd3bd5 100644 --- a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts @@ -3,9 +3,28 @@ import * as vscode from "vscode" import { ClineProvider } from "../ClineProvider" import { Task } from "../../task/Task" +import { TaskRegistry } from "../../task/TaskRegistry" import { ContextProxy } from "../../config/ContextProxy" import type { ProviderSettings, HistoryItem } from "@roo-code/types" +type MockTask = Partial & + Pick & { + parentTaskId?: string + rootTask?: { taskId: string } + parentTask?: { taskId: string } + cancelCurrentRequest?: ReturnType + isStreaming?: boolean + didFinishAbortingStream?: boolean + isWaitingForFirstChunk?: boolean + } +type CreatedHistoryTask = Awaited> + +function seedRegistry(provider: ClineProvider, ...tasks: unknown[]) { + const registry = new TaskRegistry() + for (const t of tasks) registry.push(t as unknown as Task) + provider["taskRegistry"] = registry +} + // Mock dependencies vi.mock("vscode", () => { const mockDisposable = { dispose: vi.fn() } @@ -257,10 +276,10 @@ vi.mock("../../task-persistence", async (importOriginal) => { describe("ClineProvider flicker-free cancel", () => { let provider: ClineProvider - let mockContext: any - let mockOutputChannel: any - let mockTask1: any - let mockTask2: any + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockTask1: MockTask + let mockTask2: MockTask let consoleLogSpy: ReturnType let consoleErrorSpy: ReturnType @@ -301,13 +320,13 @@ describe("ClineProvider flicker-free cancel", () => { keys: vi.fn().mockReturnValue([]), }, extensionUri: { fsPath: "/test/extension" }, - } + } as unknown as vscode.ExtensionContext // Setup mock output channel mockOutputChannel = { appendLine: vi.fn(), dispose: vi.fn(), - } + } as unknown as vscode.OutputChannel // Setup mock context proxy const mockContextProxy = { @@ -320,7 +339,12 @@ describe("ClineProvider flicker-free cancel", () => { } // Create provider instance - provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy as any) + provider = new ClineProvider( + mockContext, + mockOutputChannel, + "sidebar", + mockContextProxy as unknown as ContextProxy, + ) // Mock provider methods provider.getState = vi.fn().mockResolvedValue({ @@ -330,8 +354,8 @@ describe("ClineProvider flicker-free cancel", () => { provider.postStateToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebviewWithoutTaskHistory = vi.fn().mockResolvedValue(undefined) - // Mock private method using any cast - ;(provider as any).updateGlobalState = vi.fn().mockResolvedValue(undefined) + // Mock private method used by the rehydration path. + provider["updateGlobalState"] = vi.fn().mockResolvedValue(undefined) provider.activateProviderProfile = vi.fn().mockResolvedValue(undefined) provider.performPreparationTasks = vi.fn().mockResolvedValue(undefined) provider.getTaskWithId = vi.fn().mockImplementation((id) => @@ -371,7 +395,7 @@ describe("ClineProvider flicker-free cancel", () => { // Mock Task constructor vi.mocked(Task).mockImplementation(function () { - return mockTask2 as any + return mockTask2 as unknown as Task }) }) @@ -380,13 +404,13 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should not remove current task from stack when rehydrating same taskId", async () => { - // Setup: Add a task to the stack first - ;(provider as any).clineStack = [mockTask1] + // Setup: Add a task to the registry first + seedRegistry(provider, mockTask1) // Mock event listeners for cleanup - ;(provider as any).taskEventListeners = new WeakMap() + provider["taskEventListeners"] = new WeakMap() const mockCleanupFunctions = [vi.fn(), vi.fn()] - ;(provider as any).taskEventListeners.set(mockTask1, mockCleanupFunctions) + provider["taskEventListeners"].set(mockTask1 as unknown as Task, mockCleanupFunctions) // Spy on removeClineFromStack to verify it's NOT called const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack") @@ -410,8 +434,9 @@ describe("ClineProvider flicker-free cancel", () => { expect(removeClineFromStackSpy).not.toHaveBeenCalled() // Verify the task was replaced in-place - expect((provider as any).clineStack).toHaveLength(1) - expect((provider as any).clineStack[0]).toBe(mockTask2) + const registry = provider["taskRegistry"] + expect(registry.length).toBe(1) + expect(registry.current).toBe(mockTask2) // Verify old event listeners were cleaned up expect(mockCleanupFunctions[0]).toHaveBeenCalled() @@ -422,12 +447,12 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should remove task from stack when creating different task", async () => { - // Setup: Add a task to the stack first - ;(provider as any).clineStack = [mockTask1] + // Setup: Add a task to the registry first + seedRegistry(provider, mockTask1) // Spy on removeClineFromStack to verify it IS called const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { - ;(provider as any).clineStack.pop() + provider["taskRegistry"].pop() }) // Create history item with different taskId @@ -450,12 +475,12 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should handle empty stack gracefully during rehydration attempt", async () => { - // Setup: Empty stack - ;(provider as any).clineStack = [] + // Setup: Empty registry (default) + seedRegistry(provider) // Spy on removeClineFromStack const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { - ;(provider as any).clineStack.pop() + provider["taskRegistry"].pop() }) // Create history item @@ -478,16 +503,18 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should maintain task stack integrity during flicker-free replacement", async () => { - // Setup: Stack with multiple tasks + // Setup: Registry with parent task then current task const mockParentTask = { taskId: "parent-task", instanceId: "parent-instance", + abort: false, + abandoned: false, emit: vi.fn(), } - ;(provider as any).clineStack = [mockParentTask, mockTask1] - ;(provider as any).taskEventListeners = new WeakMap() - ;(provider as any).taskEventListeners.set(mockTask1, [vi.fn()]) + seedRegistry(provider, mockParentTask, mockTask1) + provider["taskEventListeners"] = new WeakMap() + provider["taskEventListeners"].set(mockTask1 as unknown as Task, [vi.fn()]) // Act: Rehydrate the current (top) task const historyItem: HistoryItem = { @@ -503,10 +530,50 @@ describe("ClineProvider flicker-free cancel", () => { await provider.createTaskWithHistoryItem(historyItem) - // Assert: Stack should maintain parent task and replace current task - expect((provider as any).clineStack).toHaveLength(2) - expect((provider as any).clineStack[0]).toBe(mockParentTask) - expect((provider as any).clineStack[1]).toBe(mockTask2) + // Assert: Registry should maintain parent task and replace current task + const registry = provider["taskRegistry"] + expect(registry.length).toBe(2) + expect(registry.getAll()[0]).toBe(mockParentTask) + expect(registry.getAll()[1]).toBe(mockTask2) + }) + + it("should preserve stack order when rehydrating a focused non-top task", async () => { + // Regression test for issue #1: if setCurrent() has focused a non-top task, + // rehydrating it must not move it to the top of the stack. + const mockTopTask = { + taskId: "top-task", + instanceId: "top-instance", + abort: false, + abandoned: false, + emit: vi.fn(), + } + + // Seed: [mockTask1 (focused), mockTopTask (top-of-stack)] + seedRegistry(provider, mockTask1, mockTopTask) + provider["taskRegistry"].setCurrent("task-1") + provider["taskEventListeners"] = new WeakMap() + provider["taskEventListeners"].set(mockTask1 as unknown as Task, [vi.fn()]) + + const historyItem: HistoryItem = { + id: "task-1", + number: 1, + task: "test task", + ts: Date.now(), + tokensIn: 100, + tokensOut: 200, + totalCost: 0.001, + workspace: "/test/workspace", + } + + await provider.createTaskWithHistoryItem(historyItem) + + const registry = provider["taskRegistry"] + // Stack order must be unchanged: replacement stays at index 0, top-task stays at index 1 + expect(registry.length).toBe(2) + expect(registry.getAll()[0]).toBe(mockTask2) + expect(registry.getAll()[1]).toBe(mockTopTask) + // Focus follows the replacement + expect(registry.current).toBe(mockTask2) }) it("marks a cancelled delegated child as interrupted and keeps parent delegated (preserving resume path)", async () => { @@ -552,7 +619,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -561,12 +628,12 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.resolve({ historyItem: parentHistory }) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -618,7 +685,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -627,12 +694,12 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.reject(new Error("parent lookup failed")) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -655,7 +722,7 @@ describe("ClineProvider flicker-free cancel", () => { rootTask: undefined, }), ) - expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + expect(provider["cancelledDelegationChildIds"].has("child-1")).toBe(true) }) it("does not rehydrate a cancelled child when standalone persistence also fails", async () => { @@ -683,7 +750,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -692,16 +759,16 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.reject(new Error("parent lookup failed")) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] vi.spyOn(provider, "updateTaskHistory").mockRejectedValue(new Error("standalone persist failed")) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await expect(provider.cancelTask()).rejects.toThrow("standalone persist failed") expect(createTaskWithHistoryItemSpy).not.toHaveBeenCalled() - expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + expect(provider["cancelledDelegationChildIds"].has("child-1")).toBe(true) }) it("marks a cancelled delegated child as 'interrupted' and keeps parent delegated", async () => { @@ -745,17 +812,17 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") return Promise.resolve({ historyItem: childHistory }) if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -790,15 +857,15 @@ describe("ClineProvider flicker-free cancel", () => { emit: vi.fn(), abortTask: vi.fn().mockResolvedValue(undefined), } - ;(provider as any).clineStack = [childTask] - ;(provider as any).taskEventListeners = new Map() + seedRegistry(provider, childTask) + provider["taskEventListeners"] = new Map() - provider.getTaskWithId = vi.fn() as any + provider.getTaskWithId = vi.fn() as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) - await (provider as any).removeClineFromStack() + await provider["removeClineFromStack"]() - expect((provider as any).clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(childTask.abortTask).toHaveBeenCalledWith(true) // No history writes — lifecycle only expect(updateTaskHistorySpy).not.toHaveBeenCalled() diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index dbb4fe52d8..7e111442a9 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1,6 +1,7 @@ // pnpm --filter roo-cline test core/webview/__tests__/ClineProvider.spec.ts import * as path from "path" +import { TaskRegistry } from "../../task/TaskRegistry" import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" @@ -1457,7 +1458,7 @@ describe("ClineProvider", () => { test("handles case when no current task exists", async () => { // Clear the cline stack - ;(provider as any).clineStack = [] + ;(provider as any).taskRegistry = new TaskRegistry() // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json new file mode 100644 index 0000000000..a0f00c04e3 --- /dev/null +++ b/src/eslint-suppressions.json @@ -0,0 +1,1822 @@ +{ + "__mocks__/fs/promises.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/abandonSubtask.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "__tests__/api-subtask.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/delegation-concurrent.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "__tests__/delegation-events.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "__tests__/extension.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "__tests__/history-resume-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 72 + } + }, + "__tests__/migrateSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/nested-delegation-resume.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "__tests__/new-task-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "__tests__/provider-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "activate/CodeActionProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "activate/__tests__/CodeActionProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "activate/__tests__/handleUri.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "activate/__tests__/registerCommands.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "activate/registerCodeActions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "activate/registerCommands.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "activate/registerTerminalActions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/anthropic-vertex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "api/providers/__tests__/anthropic.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/base-openai-compatible-provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/base-provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/__tests__/bedrock-custom-arn.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "api/providers/__tests__/bedrock-error-handling.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/providers/__tests__/bedrock-inference-profiles.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 27 + } + }, + "api/providers/__tests__/bedrock-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "api/providers/__tests__/bedrock-reasoning.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/bedrock.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 38 + } + }, + "api/providers/__tests__/deepseek.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "api/providers/__tests__/gemini-handler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 18 + } + }, + "api/providers/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 33 + } + }, + "api/providers/__tests__/kenari.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/kimi-code.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/lite-llm.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 36 + } + }, + "api/providers/__tests__/lm-studio-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/lmstudio.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/mimo.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 29 + } + }, + "api/providers/__tests__/minimax.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/moonshot.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "api/providers/__tests__/native-ollama.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "api/providers/__tests__/openai-codex-native-tool-calls.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 30 + } + }, + "api/providers/__tests__/openai-codex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "api/providers/__tests__/openai-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "api/providers/__tests__/openai-native-usage.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "api/providers/__tests__/openai-native.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 79 + } + }, + "api/providers/__tests__/openai-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/openai-usage-tracking.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/openai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "api/providers/__tests__/opencode-go.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "api/providers/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "api/providers/__tests__/poe.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/qwen-code-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/requesty.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/sambanova.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/unbound.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/vertex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/vscode-lm.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/xai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/zai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/anthropic-vertex.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/anthropic.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/base-openai-compatible-provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/providers/base-provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/bedrock.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "api/providers/deepseek.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/fetchers/__tests__/kenari.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/kimi-code.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/lmstudio.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/fetchers/__tests__/modelCache.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/fetchers/__tests__/modelEndpointCache.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/fetchers/__tests__/moonshot.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/ollama.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/fetchers/__tests__/opencode-go.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/zoo-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/fetchers/litellm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/gemini.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/lite-llm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/lm-studio.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/mimo.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/moonshot.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/native-ollama.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/openai-codex.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "api/providers/openai-native.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "api/providers/openai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/openrouter.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/providers/poe.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/qwen-code.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/requesty.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/unbound.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/utils/__tests__/error-handler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "api/providers/utils/__tests__/image-generation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "api/providers/utils/__tests__/timeout-config.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/utils/error-handler.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/providers/vscode-lm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/xai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/transform/__tests__/ai-sdk.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/anthropic-filter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/__tests__/bedrock-converse-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/gemini-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/__tests__/mistral-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/__tests__/model-params.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/__tests__/openai-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 49 + } + }, + "api/transform/__tests__/r1-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/__tests__/reasoning.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/__tests__/responses-api-input.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/__tests__/responses-api-stream.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/vscode-lm-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 23 + } + }, + "api/transform/__tests__/zai-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/transform/ai-sdk.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/bedrock-converse-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/cache-strategy/__tests__/cache-strategy.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "api/transform/caching/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/gemini-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/openai-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/r1-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/responses-api-input.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/responses-api-stream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/transform/vscode-lm-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/zai-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/assistant-message/NativeToolCallParser.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/assistant-message/presentAssistantMessage.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/auto-approval/__tests__/AutoApprovalHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/checkpoints/__tests__/checkpoint.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/checkpoints/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/condense/__tests__/condense.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/condense/__tests__/foldedFileContext.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "core/condense/__tests__/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 24 + } + }, + "core/config/ContextProxy.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/config/CustomModesManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/ProviderSettingsManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/config/__tests__/ContextProxy.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/__tests__/CustomModesManager.exportImportSlugChange.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/__tests__/CustomModesManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/config/__tests__/CustomModesSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/config/__tests__/ModeConfig.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "core/config/__tests__/ProviderSettingsManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/context-management/__tests__/context-management.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/context-tracking/__tests__/FileContextTracker.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/context/context-management/__tests__/context-error-handling.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/context/context-management/context-error-handling.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/diff/stats.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/environment/__tests__/getEnvironmentDetails.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/ignore/__tests__/RooIgnoreController.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/mentions/__tests__/processUserContentMentions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/mentions/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/mentions/processUserContentMentions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/message-manager/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "core/message-manager/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/prompts/__tests__/add-custom-instructions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/__tests__/get-prompt-component.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/__tests__/responses-rooignore.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "core/prompts/__tests__/system-prompt.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/prompts/sections/__tests__/custom-instructions-global.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 18 + } + }, + "core/prompts/sections/__tests__/custom-instructions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 55 + } + }, + "core/prompts/sections/__tests__/system-info.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "core/prompts/tools/filter-tools-for-mode.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/prompts/tools/native-tools/__tests__/converters.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/tools/native-tools/__tests__/read_file.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.crossInstance.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/task-persistence/__tests__/importRooTaskHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task-persistence/__tests__/taskMessages.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/task-persistence/apiMessages.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/task/Task.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "core/task/__tests__/Task.dispose.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/task/__tests__/Task.persistence.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/task/__tests__/Task.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 93 + } + }, + "core/task/__tests__/Task.sticky-profile-race.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/__tests__/Task.throttle.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 24 + } + }, + "core/task/__tests__/apiConversationHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 23 + } + }, + "core/task/__tests__/ask-clear-approval-buttons.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "core/task/__tests__/ask-queued-message-drain.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 32 + } + }, + "core/task/__tests__/flushPendingToolResultsToHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "core/task/__tests__/grace-retry-errors.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/task/__tests__/grounding-sources.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/task/__tests__/native-tools-filtering.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/__tests__/new-task-isolation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task/__tests__/reasoning-preservation.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "core/task/__tests__/task-tool-history.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/apiConversationHistory.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/tools/BaseTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/CodebaseSearchTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/GenerateImageTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/NewTaskTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/ReadFileTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/tools/ToolRepetitionDetector.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/UpdateTodoListTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/UseMcpToolTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/ReadCommandOutputTool.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/tools/__tests__/ToolRepetitionDetector.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/askFollowupQuestionTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "core/tools/__tests__/attemptCompletionTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "core/tools/__tests__/editFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/tools/__tests__/editTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/__tests__/executeCommand.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/tools/__tests__/executeCommandTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/tools/__tests__/generateImageTool.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "core/tools/__tests__/listFilesTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "core/tools/__tests__/mcpServerRestriction.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/tools/__tests__/newTaskTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "core/tools/__tests__/readFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 98 + } + }, + "core/tools/__tests__/runSlashCommandTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/searchReplaceTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/skillTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/updateTodoListTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/useMcpToolTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "core/tools/__tests__/validateToolUse.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/writeToFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/tools/helpers/toolResultFormatting.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/validateToolUse.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/webview/ClineProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "core/webview/__tests__/ClineProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 199 + } + }, + "core/webview/__tests__/ClineProvider.sticky-mode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 40 + } + }, + "core/webview/__tests__/ClineProvider.sticky-profile.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/webview/__tests__/ClineProvider.taskHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/webview/__tests__/checkpointRestoreHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/diagnosticsHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/webview/__tests__/messageEnhancer.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/webview/__tests__/skillsMessageHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/telemetrySettingsTracking.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/webview/__tests__/webviewMessageHandler.cloudAuth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/webviewMessageHandler.delete.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "core/webview/__tests__/webviewMessageHandler.edit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 13 + } + }, + "core/webview/__tests__/webviewMessageHandler.readFileContent.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 56 + } + }, + "core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/webviewMessageHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "core/webview/messageEnhancer.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/webviewMessageHandler.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "extension.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "extension/__tests__/api-delete-queued-message.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "extension/__tests__/api-send-message.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "extension/api.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "i18n/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "i18n/setup.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/editor/DiffViewProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/editor/__tests__/DiffViewProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 311 + } + }, + "integrations/editor/__tests__/EditorUtils.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "integrations/kimi-code/__tests__/oauth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/misc/__tests__/export-markdown.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/misc/__tests__/extract-text.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "integrations/misc/__tests__/line-counter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/misc/__tests__/open-file.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "integrations/terminal/__tests__/OutputInterceptor.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "integrations/terminal/__tests__/TerminalProcess.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "integrations/terminal/__tests__/TerminalProcess.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "integrations/terminal/__tests__/TerminalProfile.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "integrations/terminal/__tests__/TerminalRegistry.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "integrations/terminal/__tests__/setupTerminalTests.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/bashStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/cmdStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/streamUtils/pwshStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/theme/getTheme.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "services/__tests__/zoo-code-auth.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/checkpoints/__tests__/ShadowCheckpointService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/__tests__/config-manager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/__tests__/manager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 89 + } + }, + "services/code-index/__tests__/orchestrator.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "services/code-index/__tests__/service-factory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 43 + } + }, + "services/code-index/embedders/__tests__/bedrock.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "services/code-index/embedders/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/mistral.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/ollama.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 15 + } + }, + "services/code-index/embedders/__tests__/openai-compatible.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 28 + } + }, + "services/code-index/embedders/__tests__/openai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "services/code-index/embedders/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/bedrock.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/embedders/ollama.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/openai-compatible.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/openai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/openrouter.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/interfaces/vector-store.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/orchestrator.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/processors/__tests__/file-watcher.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "services/code-index/processors/__tests__/parser.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "services/code-index/processors/__tests__/scanner.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 26 + } + }, + "services/code-index/processors/file-watcher.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/processors/scanner.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/semble/__tests__/provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "services/code-index/semble/__tests__/semble-cli.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/semble/__tests__/semble-downloader.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 61 + } + }, + "services/code-index/semble/provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/semble/semble-cli.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/semble/semble-downloader.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/shared/__tests__/validation-helpers.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/shared/validation-helpers.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/code-index/vector-store/__tests__/qdrant-client.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 87 + } + }, + "services/code-index/vector-store/qdrant-client.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/glob/__tests__/gitignore-integration.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/glob/__tests__/gitignore-test.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/glob/__tests__/list-files-limit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "services/glob/__tests__/list-files.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "services/marketplace/MarketplaceManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/marketplace/SimpleInstaller.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "services/marketplace/__tests__/MarketplaceManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/marketplace/__tests__/SimpleInstaller.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "services/marketplace/__tests__/marketplace-setting-check.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/mcp/McpHub.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "services/mcp/McpOAuthClientProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/mcp/McpServerManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/mcp/__tests__/McpHub.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 150 + } + }, + "services/mcp/__tests__/McpOAuthClientProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "services/mcp/__tests__/SecretStorageService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/mcp/utils/__tests__/callbackServer.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/mcp/utils/__tests__/oauth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "services/mcp/utils/callbackServer.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/mcp/utils/oauth.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/mdm/__tests__/MdmService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/ripgrep/__tests__/diagnostic.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/roo-config/__tests__/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "services/roo-config/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/rules/__tests__/rules.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/search/__tests__/file-search.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/skills/__tests__/SkillsManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/tree-sitter/__tests__/helpers.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/tree-sitter/__tests__/markdownParser.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/__tests__/ProfileValidator.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/__tests__/api.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "shared/__tests__/embeddingModels.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/__tests__/modes-empty-prompt-component.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/api.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/checkExistApiConfig.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/cost.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/parse-command.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/support-prompt.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "shared/tools.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/utils/__tests__/requesty.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/__tests__/autoImportSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "utils/__tests__/enhance-prompt.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "utils/__tests__/git.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 95 + } + }, + "utils/__tests__/json-schema.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "utils/__tests__/migrateSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "utils/__tests__/outputChannelLogger.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "utils/__tests__/safeWriteJson.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 27 + } + }, + "utils/__tests__/shell.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 46 + } + }, + "utils/__tests__/storage.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "utils/__tests__/tiktoken.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/config.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/export.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/safeWriteJson.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "utils/tts.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "vitest.setup.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + } +} diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index 5c866ef941..cea212fefa 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -13,7 +13,8 @@ export default [ "no-empty": "off", "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", + // Enforced; existing violations are suppressed in eslint-suppressions.json and cleaned up incrementally. + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-require-imports": "off", "@typescript-eslint/ban-ts-comment": "off", }, From 66120b4499ab299ddb3bdefd0366f11fc273c88d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:41:23 -0400 Subject: [PATCH 63/89] chore(deps): update dependency shell-quote to v1.9.0 [security] (#986) * chore(deps): update dependency shell-quote to v1.9.0 [security] * chore(deps): remove obsolete shell-quote types --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliott de Launay --- pnpm-lock.yaml | 21 +++++---------------- src/package.json | 1 - webview-ui/package.json | 1 - 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea26770f32..9bd7df71bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -594,7 +594,7 @@ importers: version: 12.0.0 shell-quote: specifier: ^1.8.2 - version: 1.8.4 + version: 1.9.0 simple-git: specifier: ^3.27.0 version: 3.36.0 @@ -665,9 +665,6 @@ importers: '@types/semver-compare': specifier: 1.0.3 version: 1.0.3 - '@types/shell-quote': - specifier: 1.7.5 - version: 1.7.5 '@types/vscode': specifier: 1.100.0 version: 1.100.0 @@ -862,7 +859,7 @@ importers: version: 0.6.4 shell-quote: specifier: ^1.8.2 - version: 1.8.4 + version: 1.9.0 shiki: specifier: ^3.2.1 version: 3.4.1 @@ -933,9 +930,6 @@ importers: '@types/react-dom': specifier: 18.3.7 version: 18.3.7(@types/react@18.3.31) - '@types/shell-quote': - specifier: 1.7.5 - version: 1.7.5 '@types/stacktrace-js': specifier: 2.0.3 version: 2.0.3 @@ -3197,9 +3191,6 @@ packages: '@types/semver-compare@1.0.3': resolution: {integrity: sha512-mVZkB2QjXmZhh+MrtwMlJ8BqUnmbiSkpd88uOWskfwB8yitBT0tBRAKt+41VRgZD9zr9Sc+Xs02qGgvzd1Rq/Q==} - '@types/shell-quote@1.7.5': - resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} - '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -7277,8 +7268,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.4: - resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} shiki@3.4.1: @@ -10844,8 +10835,6 @@ snapshots: '@types/semver-compare@1.0.3': {} - '@types/shell-quote@1.7.5': {} - '@types/stack-utils@2.0.3': {} '@types/stacktrace-js@2.0.3': @@ -15720,7 +15709,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.4: {} + shell-quote@1.9.0: {} shiki@3.4.1: dependencies: diff --git a/src/package.json b/src/package.json index 2b022d9bd3..b4d55845b0 100644 --- a/src/package.json +++ b/src/package.json @@ -540,7 +540,6 @@ "@types/proper-lockfile": "4.1.4", "@types/ps-tree": "1.1.6", "@types/semver-compare": "1.0.3", - "@types/shell-quote": "1.7.5", "@types/vscode": "1.100.0", "@vitest/coverage-v8": "4.1.9", "@vscode/vsce": "3.9.2", diff --git a/webview-ui/package.json b/webview-ui/package.json index c947f5e005..708917bfcc 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -94,7 +94,6 @@ "@types/node": "22.20.1", "@types/react": "18.3.31", "@types/react-dom": "18.3.7", - "@types/shell-quote": "1.7.5", "@types/stacktrace-js": "2.0.3", "@types/vscode-webview": "1.57.5", "@vitejs/plugin-react": "5.2.0", From 1ceb4e6998458a6a1125176ba5a49eaaa60bb703 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:28:00 -0400 Subject: [PATCH 64/89] test(webview): add Playwright visual regression harness (#526) * test(webview): add visual regression harness * fix(webview): resolve relative compose path & font race in visual tests * test(webview): configure Codecov JSDOM coverage ratcheting * feat(webview): add Playwright CT coverage ratcheting * fix(webview): swap visual regression target off deprecated toolkit * chore(webview): address CodeRabbit review feedback * fix(webview): emit Playwright CT coverage LCOV * fix(webview): address code review feedback for Playwright CT harness * docs: prefer committed visual snapshots; split videos into their own lane * revert(RooHero): removing on hover test - can't make it deterministic --- .github/pull_request_template.md | 27 +- .github/workflows/code-qa.yml | 15 +- .github/workflows/visual-regression.yml | 57 +++ codecov.yml | 34 +- knip.json | 6 +- pnpm-lock.yaml | 339 ++++++++++++++++++ webview-ui/.gitignore | 6 + webview-ui/AGENTS.md | 64 ++++ webview-ui/docker-compose.visual.yml | 12 + webview-ui/package.json | 6 + webview-ui/playwright-ct.config.ts | 95 +++++ webview-ui/playwright/coverage-fixture.ts | 23 ++ webview-ui/playwright/index.html | 12 + webview-ui/playwright/index.tsx | 9 + webview-ui/playwright/run-docker.mjs | 53 +++ webview-ui/playwright/vscode-theme-dark.css | 89 +++++ .../welcome/__tests__/RooHero.visual.tsx | 29 ++ .../__screenshots__/zoo-hero-dark.png | Bin 0 -> 20809 bytes webview-ui/tsconfig.json | 2 +- webview-ui/vitest.config.ts | 4 +- 20 files changed, 870 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/visual-regression.yml create mode 100644 webview-ui/docker-compose.visual.yml create mode 100644 webview-ui/playwright-ct.config.ts create mode 100644 webview-ui/playwright/coverage-fixture.ts create mode 100644 webview-ui/playwright/index.html create mode 100644 webview-ui/playwright/index.tsx create mode 100644 webview-ui/playwright/run-docker.mjs create mode 100644 webview-ui/playwright/vscode-theme-dark.css create mode 100644 webview-ui/src/components/welcome/__tests__/RooHero.visual.tsx create mode 100644 webview-ui/src/components/welcome/__tests__/__screenshots__/zoo-hero-dark.png diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cfcb93de39..7960d608eb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -38,14 +38,35 @@ Detail the steps to test your changes. This helps reviewers verify your work. - [ ] **Scope**: My changes are focused on the linked issue (one major feature/fix per PR). - [ ] **Self-Review**: I have performed a thorough self-review of my code. - [ ] **Testing**: New and/or updated tests have been added to cover my changes (if applicable). +- [ ] **Visual Snapshot** (UI changes only): If a user would notice this change at a glance (layout, theme tokens, brand elements, empty/error states), I've added or updated a `*.visual.tsx` snapshot in `webview-ui/`. See `webview-ui/AGENTS.md` → "When a UI change needs a snapshot". - [ ] **Documentation Impact**: I have considered if my changes require documentation updates (see "Documentation Updates" section below). - [ ] **Contribution Guidelines**: I have read and agree to the [Contributor Guidelines](/CONTRIBUTING.md). -### Screenshots / Videos +### Visual Snapshots + +### Videos (interaction / animation only) + + ### Documentation Updates diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index f0e7ff0aa7..21d1f29fff 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -135,11 +135,11 @@ jobs: # lanes for behavioral confidence, but duplicating coverage uploads # there mostly adds Codecov overhead without changing pass/fail # behavior. - # Coverage is uploaded in three separate steps so each LCOV gets the + # Coverage is uploaded in separate steps so each LCOV gets the # correct flag set. Codecov double-counts overlapping lines when a # single upload carries multiple flags whose paths overlap, so the - # two core lanes (which both cover packages/core/src/**) must be - # uploaded individually with their own lane flag. + # core lanes and webview lane must be uploaded individually with + # their own flag. # See https://docs.codecov.com/docs/flags - name: Upload non-core coverage to Codecov if: matrix.upload-coverage @@ -147,13 +147,20 @@ jobs: with: files: >- src/coverage/lcov.info, - webview-ui/coverage/lcov.info, packages/cloud/coverage/lcov.info, packages/telemetry/coverage/lcov.info, apps/cli/coverage/lcov.info disable_search: true flags: ${{ matrix.codecov-flag }} token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload webview JSDOM coverage to Codecov + if: matrix.upload-coverage + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 + with: + files: webview-ui/coverage/lcov.info + disable_search: true + flags: webview-ui + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload core unit coverage to Codecov if: matrix.upload-coverage uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml new file mode 100644 index 0000000000..6160ea6be0 --- /dev/null +++ b/.github/workflows/visual-regression.yml @@ -0,0 +1,57 @@ +name: Webview Visual Regression + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, ready_for_review, synchronize] + paths: + - "webview-ui/**" + - "src/shared/**" + - "package.json" + - "pnpm-lock.yaml" + - ".github/workflows/visual-regression.yml" + merge_group: + types: [checks_requested] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + webview-visual: + runs-on: ubuntu-latest + timeout-minutes: 10 + container: + image: mcr.microsoft.com/playwright:v1.60.0-noble@sha256:9bd26ad900bb5e0f4dee75839e957a89ae89c2b7ab1e76050e559790e946b948 + options: --ipc=host + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + install-args: "--frozen-lockfile" + - name: Run webview visual tests + run: pnpm --filter @roo-code/vscode-webview test:visual + - name: Upload visual test coverage to Codecov + if: always() + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 + with: + files: webview-ui/coverage-ct/lcov.info + disable_search: true + flags: webview-ui-ct + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload visual test artifacts + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: webview-visual-regression + path: | + webview-ui/playwright-report + webview-ui/test-results + if-no-files-found: ignore diff --git a/codecov.yml b/codecov.yml index 6238bd8f62..7dd22dfdc2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,13 +6,33 @@ coverage: default: target: auto # never regress below current baseline threshold: 1% + webview: + target: auto # webview project ratchet: never drop below current baseline + threshold: 0.5% + flags: + - webview-ui + - webview-ui-ct patch: default: target: 80% # new lines must be 80% covered threshold: 0% + webview-patch: + target: 70% # new lines in webview must be 70% covered + threshold: 0% + flags: + - webview-ui + - webview-ui-ct flag_management: individual_flags: + - name: webview-ui + paths: + - webview-ui/src/ + carryforward: true + - name: webview-ui-ct + paths: + - webview-ui/src/ + carryforward: true - name: core-unit paths: - packages/core/src/ @@ -22,6 +42,18 @@ flag_management: - packages/core/src/ carryforward: true +component_management: + individual_components: + - component_id: webview_components + name: "Webview UI Components" + paths: + - webview-ui/src/components/ + - component_id: webview_state + name: "Webview State & Context" + paths: + - webview-ui/src/context/ + - webview-ui/src/state/ + comment: - layout: "diff, flags" + layout: "diff, flags, components" behavior: default diff --git a/knip.json b/knip.json index 1e0771ef9b..db102031eb 100644 --- a/knip.json +++ b/knip.json @@ -1,8 +1,10 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "ignore": ["**/__tests__/**", "apps/vscode-e2e/**", "scripts/**", "apps/cli/scripts/**"], - "ignoreDependencies": ["lint-staged", "ovsx"], + "ignoreDependencies": ["lint-staged"], "ignoreExportsUsedInFile": true, + "playwright": false, + "playwright-ct": false, "workspaces": { "src": { "entry": ["extension.ts", "extension/api.ts", "workers/countTokens.ts"], @@ -30,7 +32,7 @@ "source-map", "tailwindcss", "tailwindcss-animate", - "vscode" + "monocart-reporter" ] }, "apps/cli": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bd7df71bd..7c3dd070ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -897,6 +897,12 @@ importers: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@playwright/experimental-ct-react': + specifier: 1.60.0 + version: 1.60.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) + '@playwright/test': + specifier: 1.60.0 + version: 1.60.0 '@roo-code/config-eslint': specifier: workspace:^ version: link:../packages/config-eslint @@ -951,6 +957,9 @@ importers: jsdom: specifier: 26.1.0 version: 26.1.0 + monocart-reporter: + specifier: ^2.9.20 + version: 2.12.2 vite: specifier: 8.1.0 version: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) @@ -1966,6 +1975,20 @@ packages: cpu: [x64] os: [win32] + '@playwright/experimental-ct-core@1.60.0': + resolution: {integrity: sha512-cMYDe0KpfdfFTZR2ev6frlgIwksSnXCu1o5s/DxpiVPTJeKp8TSXo9dzKuRhUMDAxAQESF1uM4uPQ56PZ/a5QA==} + engines: {node: '>=18'} + + '@playwright/experimental-ct-react@1.60.0': + resolution: {integrity: sha512-PT9RbpLO1nRtIcG/odJKM0m0UjQjLKYOcjs52UpASb8fEWovInHGi10Tw6JXl/3aMgSayX2hUzhYd8+yRFxj4w==} + engines: {node: '>=18'} + hasBin: true + + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2531,6 +2554,9 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -3281,6 +3307,12 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: 8.1.0 + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3466,6 +3498,10 @@ packages: '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -3475,11 +3511,24 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-loose@8.5.2: + resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -4002,10 +4051,17 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + console-grid@2.2.4: + resolution: {integrity: sha512-OLjCRTiHhOpTRo9lQp/2FgJDyq5uQHwkEmVJulEnQ6JVf27oKKzXHZnNOv/e72V4++UdMZCrDWtvXW5sx4lyQg==} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -4025,6 +4081,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -4305,6 +4365,9 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4347,6 +4410,13 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4355,6 +4425,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -4463,6 +4537,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + eight-colors@1.3.3: + resolution: {integrity: sha512-4B54S2Qi4pJjeHmCbDIsveQZWQ/TSSQng4ixYJ9/SYHHpeS5nYK0pzcHvWzWUfRsvJQjwoIENhAwqg59thQceg==} + electron-to-chromium@1.5.152: resolution: {integrity: sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==} @@ -4848,6 +4925,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + foreground-child@4.0.3: + resolution: {integrity: sha512-yeXZaNbCBGaT9giTpLPBdtedzjwhlJBUoL/R4BVQU5mn0TQXOHwVIl1Q2DMuBIdNno4ktA1abZ7dQFVxD6uHxw==} + engines: {node: '>=16'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -4865,6 +4946,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4887,6 +4972,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5133,6 +5223,14 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -5692,6 +5790,10 @@ packages: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + keytar@7.9.0: resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} @@ -5713,6 +5815,16 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4' + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa-static-resolver@1.0.6: + resolution: {integrity: sha512-ZX5RshSzH8nFn05/vUNQzqw32nEigsPa67AVUr6ZuQxuGdnCcTLcdgr4C81+YbJjpgqKHfacMBd7NmJIbj7fXw==} + + koa@3.2.1: + resolution: {integrity: sha512-e7IpWJrnanNUroVK2taAgMxoEZvHLXdQiNjeExSu/DEIWm83jaKGBgb7tLmu2rMYpA027qFB3iLR/k3AVpFRnA==} + engines: {node: '>= 18'} + layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -5959,6 +6071,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + lz-utils@2.1.1: + resolution: {integrity: sha512-d3Thjos0PSJQAoyMj6vipSSrtrRHS7DImqUNR8x9NW3+zQIftPIbMJAWhi5nPdg5Q9zHz6lxtN8kp/VdMlhi/Q==} + macos-release@3.3.0: resolution: {integrity: sha512-tPJQ1HeyiU2vRruNGhZ+VleWuMQRro8iFtJxYgnS4NQe+EukKF6aGiIT+7flZhISAt2iaXBCfFGvAyif7/f8nQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6276,6 +6391,17 @@ packages: peerDependencies: tslib: ^2.0.1 + monocart-coverage-reports@2.12.12: + resolution: {integrity: sha512-d9FdUr2dn58Crweon0IE0zVi8r/i4vLvfvb2G7eL5vL5LfrxCB2X6F6qzuiwV6RioA4zbAI//7CYi6LjCvN3zA==} + hasBin: true + + monocart-locator@1.0.3: + resolution: {integrity: sha512-pe29W2XAoA1WQmZZqxXoP7s06ZEXUhcb81086v68cqjk1HnVL7Q/iU/WJnnetxjPcLqwb4qG8vaSGUOMQU602g==} + + monocart-reporter@2.12.2: + resolution: {integrity: sha512-LkQQVeZvpYht4AmR76lBZj21pUxRKtTUmISktSbYQyDw09egqNJ3GJiM5d3QvtfobpogY8SdmEXml8wF9Rkrfw==} + hasBin: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -6310,6 +6436,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -6667,6 +6797,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + pluralize@2.0.0: resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==} @@ -6885,6 +7025,10 @@ packages: peerDependencies: react: ^19.2.0 + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -7410,6 +7554,10 @@ packages: standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -7761,6 +7909,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -9640,6 +9792,49 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.21.3': optional: true + '@playwright/experimental-ct-core@1.60.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)': + dependencies: + playwright: 1.60.0 + playwright-core: 1.60.0 + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@playwright/experimental-ct-react@1.60.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)': + dependencies: + '@playwright/experimental-ct-core': 1.60.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + '@vitejs/plugin-react': 4.7.0(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - vite + - yaml + + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@polka/url@1.0.0-next.29': {} '@posthog/core@1.38.0': @@ -10174,6 +10369,8 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rolldown/pluginutils@1.0.1': {} @@ -10952,6 +11149,18 @@ snapshots: '@vercel/oidc@3.2.0': {} + '@vitejs/plugin-react@4.7.0(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 @@ -11181,6 +11390,11 @@ snapshots: '@xobotyi/scrollbar-width@1.9.5': {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -11190,8 +11404,18 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-loose@8.5.2: + dependencies: + acorn: 8.17.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.17.0 + acorn@8.15.0: {} + acorn@8.17.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -11744,10 +11968,14 @@ snapshots: consola@3.4.2: {} + console-grid@2.2.4: {} + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} convert-source-map@2.0.0: {} @@ -11758,6 +11986,11 @@ snapshots: cookie@0.7.2: {} + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -12078,6 +12311,8 @@ snapshots: mimic-response: 3.1.0 optional: true + deep-equal@1.0.1: {} + deep-extend@0.6.0: optional: true @@ -12114,10 +12349,16 @@ snapshots: delayed-stream@1.0.0: {} + delegates@1.0.0: {} + + depd@1.1.2: {} + depd@2.0.0: {} dequal@2.0.3: {} + destroy@1.2.0: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -12217,6 +12458,8 @@ snapshots: ee-first@1.1.1: {} + eight-colors@1.3.3: {} + electron-to-chromium@1.5.152: {} emoji-regex@10.4.0: {} @@ -12760,6 +13003,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + foreground-child@4.0.3: + dependencies: + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -12778,6 +13025,8 @@ snapshots: forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} from@0.1.7: {} @@ -12802,6 +13051,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -13133,6 +13385,19 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -13697,6 +13962,10 @@ snapshots: dependencies: commander: 8.3.0 + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + keytar@7.9.0: dependencies: node-addon-api: 4.3.0 @@ -13731,6 +14000,31 @@ snapshots: zod: 3.25.76 zod-validation-error: 3.5.4(zod@3.25.76) + koa-compose@4.1.0: {} + + koa-static-resolver@1.0.6: {} + + koa@3.2.1: + dependencies: + accepts: 1.3.8 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookies: 0.9.1 + delegates: 1.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 2.0.1 + koa-compose: 4.1.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + layout-base@1.0.2: {} layout-base@2.0.1: {} @@ -13935,6 +14229,8 @@ snapshots: lz-string@1.5.0: {} + lz-utils@2.1.1: {} + macos-release@3.3.0: {} magic-string@0.30.21: @@ -14520,6 +14816,33 @@ snapshots: fs-extra: 7.0.1 tslib: 2.8.1 + monocart-coverage-reports@2.12.12: + dependencies: + acorn: 8.17.0 + acorn-loose: 8.5.2 + acorn-walk: 8.3.5 + commander: 14.0.3 + console-grid: 2.2.4 + eight-colors: 1.3.3 + foreground-child: 4.0.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + lz-utils: 2.1.1 + monocart-locator: 1.0.3 + + monocart-locator@1.0.3: {} + + monocart-reporter@2.12.2: + dependencies: + console-grid: 2.2.4 + eight-colors: 1.3.3 + koa: 3.2.1 + koa-static-resolver: 1.0.6 + lz-utils: 2.1.1 + monocart-coverage-reports: 2.12.12 + monocart-locator: 1.0.3 + mri@1.2.0: {} mrmime@2.0.1: {} @@ -14554,6 +14877,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@1.0.0: {} nock@14.0.15: @@ -14943,6 +15268,14 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@2.0.0: {} pluralize@8.0.0: {} @@ -15181,6 +15514,8 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-refresh@0.17.0: {} + react-refresh@0.18.0: {} react-remark@2.1.0(react@18.3.1): @@ -15873,6 +16208,8 @@ snapshots: '@stablelib/base64': 1.0.1 fast-sha256: 1.3.0 + statuses@1.5.0: {} + statuses@2.0.2: {} std-env@4.1.0: {} @@ -16221,6 +16558,8 @@ snapshots: tslib@2.8.1: {} + tsscmp@1.0.6: {} + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) diff --git a/webview-ui/.gitignore b/webview-ui/.gitignore index 1f81cba3f5..6d6c359019 100644 --- a/webview-ui/.gitignore +++ b/webview-ui/.gitignore @@ -7,6 +7,12 @@ # testing /coverage +/coverage-ct +/monocart-report +/assets/monocart-coverage-app.js +/playwright-report +/test-results +/playwright/.cache # production /build diff --git a/webview-ui/AGENTS.md b/webview-ui/AGENTS.md index adab48e9e0..c118e36c9a 100644 --- a/webview-ui/AGENTS.md +++ b/webview-ui/AGENTS.md @@ -2,7 +2,71 @@ This file provides guidance to agents working in `webview-ui/`. +## Testing Strategy Overview + +We use a complementary two-layer strategy for testing webview UI code: + +1. **Vitest + JSDOM (`*.test.tsx`)**: Unit, hook, state-machine, and interaction tests. +2. **Playwright Component Testing (`*.visual.tsx`)**: Visual snapshot, VS Code theme variable, layout, and shadow DOM tests. + +--- + +### When to write a JSDOM Test (`*.test.tsx`) vs. a Playwright Visual Test (`*.visual.tsx`) + +| Testing Goal | Recommended Harness | +| :-------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| Component state transitions, reducer actions, custom hook behavior | **Vitest + JSDOM** (`*.test.tsx`) | +| User interactions (button clicks, form validation, text typing) | **Vitest + JSDOM** (`*.test.tsx`) using `@testing-library/user-event` | +| Conditional DOM rendering or prop wiring | **Vitest + JSDOM** (`*.test.tsx`) | +| Visual layout, flexbox/grid alignment, or padding/margin verification | **Playwright CT** (`*.visual.tsx`) | +| VS Code dark/light theme CSS tokens (`--vscode-*`) | **Playwright CT** (`*.visual.tsx`) | +| Web component shadow DOM style encapsulation & upgrades | **Playwright CT** (`*.visual.tsx`) | + +--- + +## Unit & State Tests (Vitest + JSDOM) + - Prefer local `webview-ui` tests for React/webview behavior. If a change is about component rendering, local state, hooks, form dirty-state, validation, or prop wiring inside the webview, add or update Vitest coverage under `webview-ui/src/**/__tests__` instead of reaching for `apps/vscode-e2e`. - Use `apps/vscode-e2e` only when the behavior depends on the real VS Code extension environment: extension-host to webview messaging, VS Code workspace APIs, task execution flows, or other end-to-end behavior that needs `@vscode/test-electron`. - When a regression can be proven with a component or webview integration test, keep it in `webview-ui`. Do not promote it to e2e just because the UI is hosted inside VS Code. - For `SettingsView`, preserve the cached-state pattern from the repo root guidance: inputs should operate on local `cachedState` until the user saves, and tests should distinguish automatic initialization from real user edits. + +### Coverage & Codecov Quality Gates + +Codecov tracks `webview-ui` coverage under the `webview-ui` flag. + +- **Ratcheting (`target: auto`)**: Overall webview coverage will never drop below the current baseline as new tests are added. +- **Patch Gate (`target: 70%`)**: New or modified lines in PRs touching `webview-ui/src/` must meet minimum test coverage, ensuring state changes and new UI logic stay tested over time. + +--- + +## Visual Tests (Playwright CT) + +### When a UI change needs a snapshot + +If your PR changes anything a user would notice at a glance — layout, spacing, theme tokens, brand elements, gradients, mask/blur effects, hover/empty/error states — **add a `*.visual.tsx` snapshot to the same PR**. Do not attach screenshots to the PR description as evidence; commit the baseline instead so future PRs get regression coverage automatically. + +Visual regression is screen-based, not line-based — the goal is a small set of durable "pixel receipts" for the surfaces users see first, not blanket coverage. Prefer covering: + +- Onboarding / first-run surfaces (welcome view, hero, unconfigured state) +- Empty and error states (no history, provider misconfig, degraded modes) +- Theme-critical layouts that rely heavily on `--vscode-*` tokens or CSS masks/gradients +- One representative snapshot per user-facing screen — not per component + +Skip a visual test when the change is behavior-only (state transitions, handler wiring, validation) — those belong in Vitest. Visual tests are for what JSDOM cannot verify. + +### Where the two coverage flags fit + +- `webview-ui` (Vitest + JSDOM) — broad line coverage over component logic, hooks, and state. This is your main coverage gate. +- `webview-ui-ct` (Playwright CT) — narrow pixel-regression signal over a small set of critical screens. Low absolute % is expected and fine; the flag is not a coverage-to-hit target, it's a "did the surface still render the same" check. + +### Authoring rules + +- Keep behavioral assertions in Vitest. A `*.visual.tsx` test should establish a deterministic state and make a focused screenshot assertion. +- Run visual comparisons with `pnpm test:visual:docker` from `webview-ui/`. +- Update intentional baselines with `pnpm test:visual:docker:update` and commit the resulting `__screenshots__` files with the UI change. +- Use the Docker commands when creating or reviewing baselines; host-rendered screenshots are not the source of truth. +- If Docker is unavailable, `pnpm test:visual` can help diagnose test code, but do not create or update committed baselines from the host rendering environment. +- Keep visual tests limited to components supported by the current Playwright harness. Add shared extension state, translation, React Query, or other provider support before snapshotting components that require it. +- The current baseline naming assumes a single Chromium project. Include `{projectName}` in `snapshotPathTemplate` before adding another browser project. +- Import `test` and `expect` from `webview-ui/playwright/coverage-fixture.ts` (not directly from `@playwright/experimental-ct-react`) so the auto-fixture collects V8 coverage for `monocart-reporter` — that's what produces `coverage-ct/lcov.info` for the Codecov upload. diff --git a/webview-ui/docker-compose.visual.yml b/webview-ui/docker-compose.visual.yml new file mode 100644 index 0000000000..0c5ee01832 --- /dev/null +++ b/webview-ui/docker-compose.visual.yml @@ -0,0 +1,12 @@ +services: + visual: + image: mcr.microsoft.com/playwright:v1.60.0-noble@sha256:9bd26ad900bb5e0f4dee75839e957a89ae89c2b7ab1e76050e559790e946b948 + ipc: host + user: "${UID:-1000}:${GID:-1000}" + working_dir: /work + environment: + COREPACK_HOME: /work/node_modules/.cache/corepack + HOME: /tmp/playwright + volumes: + - ..:/work + command: sh -lc "corepack pnpm --filter @roo-code/vscode-webview test:visual" diff --git a/webview-ui/package.json b/webview-ui/package.json index 708917bfcc..83777bcbf1 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -8,6 +8,9 @@ "pretest": "turbo run bundle --cwd ..", "test": "vitest run", "test:coverage": "vitest run --coverage", + "test:visual": "playwright test -c playwright-ct.config.ts", + "test:visual:docker": "node playwright/run-docker.mjs", + "test:visual:docker:update": "node playwright/run-docker.mjs --update", "format": "prettier --write src", "dev": "vite", "build": "tsc -b && vite build", @@ -83,6 +86,8 @@ "zod": "^3.25.61" }, "devDependencies": { + "@playwright/experimental-ct-react": "1.60.0", + "@playwright/test": "1.60.0", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", "@testing-library/jest-dom": "6.9.1", @@ -101,6 +106,7 @@ "@vitest/ui": "4.1.9", "babel-plugin-react-compiler": "1.0.0", "jsdom": "26.1.0", + "monocart-reporter": "^2.9.20", "vite": "8.1.0", "vitest": "4.1.9" } diff --git a/webview-ui/playwright-ct.config.ts b/webview-ui/playwright-ct.config.ts new file mode 100644 index 0000000000..8b236781b2 --- /dev/null +++ b/webview-ui/playwright-ct.config.ts @@ -0,0 +1,95 @@ +import path from "path" +import { fileURLToPath } from "url" + +import { defineConfig } from "@playwright/experimental-ct-react" +import type { ReporterDescription } from "@playwright/test" +import react from "@vitejs/plugin-react" +import tailwindcss from "@tailwindcss/vite" + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +const monocartReporter: ReporterDescription = [ + "monocart-reporter", + { + name: "Webview Playwright CT", + outputFile: path.resolve(dirname, "coverage-ct/index.html"), + coverage: { + outputDir: path.resolve(dirname, "coverage-ct"), + reports: ["lcovonly", "v8"], + // Entry URLs from Vite CT dev server or built bundle. + entryFilter: (entry: { url: string }) => entry.url.includes("/src/") || entry.url.includes("/assets/"), + sourceFilter: (sourcePath: string) => + sourcePath.includes("src/") && + !sourcePath.includes(".visual.") && + !sourcePath.includes(".spec.") && + !sourcePath.includes(".test."), + }, + }, +] + +export default defineConfig({ + testDir: "./src", + testMatch: "**/*.visual.tsx", + outputDir: path.resolve(dirname, "test-results"), + snapshotPathTemplate: "{testDir}/{testFileDir}/__screenshots__/{arg}{ext}", + fullyParallel: true, + reporter: process.env.CI + ? [ + ["html", { open: "never", outputFolder: path.resolve(dirname, "playwright-report") }], + ["github"], + ["list"], + monocartReporter, + ] + : [ + ["html", { open: "never", outputFolder: path.resolve(dirname, "playwright-report") }], + ["list"], + monocartReporter, + ], + use: { + ctTemplateDir: "./playwright", + ctViteConfig: { + plugins: [ + react({ + babel: { + plugins: [["babel-plugin-react-compiler", { target: "18" }]], + }, + }), + tailwindcss(), + ], + resolve: { + alias: { + "@": path.resolve(dirname, "./src"), + "@src": path.resolve(dirname, "./src"), + "@roo": path.resolve(dirname, "../src/shared"), + vscode: path.resolve(dirname, "./src/__mocks__/vscode.ts"), + }, + }, + define: { + "process.platform": JSON.stringify(process.platform), + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "test"), + "process.env.PKG_NAME": JSON.stringify("zoo-code"), + "process.env.PKG_VERSION": JSON.stringify("0.0.0-test"), + "process.env.PKG_OUTPUT_CHANNEL": JSON.stringify("Zoo-Code"), + "process.env.PKG_RELEASE_CHANNEL": JSON.stringify("stable"), + }, + optimizeDeps: { + exclude: ["@vscode/codicons"], + }, + publicDir: path.resolve(dirname, "../src/assets/images"), + }, + viewport: { width: 520, height: 360 }, + deviceScaleFactor: 1, + colorScheme: "dark", + }, + expect: { + toHaveScreenshot: { + animations: "disabled", + }, + }, + projects: [ + { + name: "chromium", + use: { browserName: "chromium" }, + }, + ], +}) diff --git a/webview-ui/playwright/coverage-fixture.ts b/webview-ui/playwright/coverage-fixture.ts new file mode 100644 index 0000000000..d316f32053 --- /dev/null +++ b/webview-ui/playwright/coverage-fixture.ts @@ -0,0 +1,23 @@ +import { test as base, expect } from "@playwright/experimental-ct-react" +import { addCoverageReport } from "monocart-reporter" + +// Auto-fixture that collects V8 JS + CSS coverage per test and hands it to the +// monocart-reporter attached in `playwright-ct.config.ts`, which writes the +// LCOV consumed by Codecov. Every `.visual.tsx` file imports `test`/`expect` +// from here so coverage is collected without per-test boilerplate. +export const test = base.extend<{ collectCoverage: void }>({ + collectCoverage: [ + async ({ page }, use, testInfo) => { + await Promise.all([ + page.coverage.startJSCoverage({ resetOnNavigation: false }), + page.coverage.startCSSCoverage({ resetOnNavigation: false }), + ]) + await use() + const [js, css] = await Promise.all([page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage()]) + await addCoverageReport([...js, ...css], testInfo) + }, + { auto: true }, + ], +}) + +export { expect } diff --git a/webview-ui/playwright/index.html b/webview-ui/playwright/index.html new file mode 100644 index 0000000000..144ced8a48 --- /dev/null +++ b/webview-ui/playwright/index.html @@ -0,0 +1,12 @@ + + + + + + webview-ui visual tests + + +
+ + + diff --git a/webview-ui/playwright/index.tsx b/webview-ui/playwright/index.tsx new file mode 100644 index 0000000000..67af70105d --- /dev/null +++ b/webview-ui/playwright/index.tsx @@ -0,0 +1,9 @@ +import "./vscode-theme-dark.css" +import "@vscode/codicons/dist/codicon.css" +import "../src/index.css" + +// Components read `window.IMAGES_BASE_URI` at mount time to resolve extension +// image assets. Under Playwright CT the extension host isn't present, so seed +// it to serve from the CT bundle root (mapped to src/assets/images via +// `publicDir` in playwright-ct.config.ts). +;(window as unknown as { IMAGES_BASE_URI: string }).IMAGES_BASE_URI = "" diff --git a/webview-ui/playwright/run-docker.mjs b/webview-ui/playwright/run-docker.mjs new file mode 100644 index 0000000000..cecd80bb31 --- /dev/null +++ b/webview-ui/playwright/run-docker.mjs @@ -0,0 +1,53 @@ +import { spawn, spawnSync } from "node:child_process" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const dirname = path.dirname(fileURLToPath(import.meta.url)) +const composeFile = path.resolve(dirname, "../docker-compose.visual.yml") + +const updateSnapshots = process.argv.includes("--update") +const composeArgs = ["-f", composeFile, "run", "--rm", "visual"] + +if (updateSnapshots) { + // Inlined so host-rendered baselines aren't reachable via a `pnpm` script. + composeArgs.push( + "sh", + "-lc", + "corepack pnpm --filter @roo-code/vscode-webview exec playwright test -c playwright-ct.config.ts --update-snapshots", + ) +} + +// Compose falls back to UID 1000 if not passed; that mis-owns generated files. +const spawnEnv = { + ...process.env, + UID: String(process.getuid?.() ?? 1000), + GID: String(process.getgid?.() ?? 1000), +} + +const hasComposePlugin = spawnSync("docker", ["compose", "version"], { stdio: "ignore" }).status === 0 +const command = hasComposePlugin ? "docker" : "docker-compose" +const args = hasComposePlugin ? ["compose", ...composeArgs] : composeArgs + +const child = spawn(command, args, { stdio: "inherit", env: spawnEnv }) + +const forwardSignal = (signal) => { + if (child.pid && !child.killed) { + child.kill(signal) + } +} + +process.on("SIGINT", () => forwardSignal("SIGINT")) +process.on("SIGTERM", () => forwardSignal("SIGTERM")) + +child.on("error", (err) => { + console.error(`Unable to run ${command}: ${err.message}`) + process.exit(1) +}) + +child.on("close", (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + } else { + process.exit(code ?? 1) + } +}) diff --git a/webview-ui/playwright/vscode-theme-dark.css b/webview-ui/playwright/vscode-theme-dark.css new file mode 100644 index 0000000000..30d49f968b --- /dev/null +++ b/webview-ui/playwright/vscode-theme-dark.css @@ -0,0 +1,89 @@ +:root { + color-scheme: dark; + --vscode-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --vscode-font-size: 13px; + --vscode-font-weight: normal; + --vscode-editor-font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + --vscode-editor-font-size: 12px; + --vscode-editor-line-height: 18px; + --vscode-foreground: #cccccc; + --vscode-disabledForeground: #808080; + --vscode-descriptionForeground: #9d9d9d; + --vscode-errorForeground: #f48771; + --vscode-focusBorder: #007fd4; + --vscode-editor-foreground: #d4d4d4; + --vscode-editor-background: #1e1e1e; + --vscode-editorGroup-border: #2b2b2b; + --vscode-editorWarning-foreground: #cca700; + --vscode-editorWarning-background: #352a05; + --vscode-button-foreground: #ffffff; + --vscode-button-background: #0e639c; + --vscode-button-hoverBackground: #1177bb; + --vscode-button-secondaryForeground: #cccccc; + --vscode-button-secondaryBackground: #3a3d41; + --vscode-dropdown-foreground: #f0f0f0; + --vscode-dropdown-background: #3c3c3c; + --vscode-dropdown-border: #3c3c3c; + --vscode-input-foreground: #cccccc; + --vscode-input-background: #3c3c3c; + --vscode-input-border: #3c3c3c; + --vscode-badge-foreground: #ffffff; + --vscode-badge-background: #4d4d4d; + --vscode-notifications-foreground: #cccccc; + --vscode-notifications-background: #252526; + --vscode-notifications-border: #303031; + --vscode-list-hoverForeground: #cccccc; + --vscode-list-hoverBackground: #2a2d2e; + --vscode-list-focusBackground: #04395e; + --vscode-list-activeSelectionBackground: #04395e; + --vscode-list-activeSelectionForeground: #ffffff; + --vscode-toolbar-hoverBackground: #2a2d2e; + --vscode-toolbar-hoverOutline: #00000000; + --vscode-panel-border: #2b2b2b; + --vscode-progressBar-background: #0e70c0; + --vscode-sideBar-foreground: #cccccc; + --vscode-sideBar-background: #252526; + --vscode-sideBar-border: #2b2b2b; + --vscode-sideBarSectionHeader-foreground: #cccccc; + --vscode-sideBarSectionHeader-background: #00000000; + --vscode-sideBarSectionHeader-border: #2b2b2b; + --vscode-titleBar-activeForeground: #cccccc; + --vscode-titleBar-inactiveForeground: #999999; + --vscode-charts-blue: #3794ff; + --vscode-charts-green: #89d185; + --vscode-charts-orange: #d18616; + --vscode-charts-red: #f14c4c; + --vscode-charts-yellow: #cca700; + --vscode-inputValidation-infoForeground: #d4d4d4; + --vscode-inputValidation-infoBackground: #063b49; + --vscode-inputValidation-infoBorder: #007acc; + --vscode-inputValidation-warningForeground: #d4d4d4; + --vscode-inputValidation-warningBackground: #352a05; + --vscode-inputValidation-warningBorder: #b89500; + --vscode-widget-border: #303031; + --vscode-widget-shadow: #0000005c; + --vscode-textLink-foreground: #3794ff; + --vscode-textCodeBlock-background: #2d2d2d; + --vscode-menu-foreground: #cccccc; + --vscode-menu-background: #252526; + --vscode-editorHoverWidget-foreground: #cccccc; + --vscode-editorHoverWidget-background: #252526; + --vscode-editorHoverWidget-border: #454545; + --vscode-banner-background: #04395e; + --vscode-banner-foreground: #ffffff; + --vscode-scrollbarSlider-background: #79797966; + --vscode-scrollbarSlider-hoverBackground: #646464b3; + --vscode-scrollbarSlider-activeBackground: #bfbfbf66; +} + +body { + margin: 0; + background: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); +} + +#root { + padding: 16px; +} diff --git a/webview-ui/src/components/welcome/__tests__/RooHero.visual.tsx b/webview-ui/src/components/welcome/__tests__/RooHero.visual.tsx new file mode 100644 index 0000000000..8b9c8b46dd --- /dev/null +++ b/webview-ui/src/components/welcome/__tests__/RooHero.visual.tsx @@ -0,0 +1,29 @@ +import React from "react" + +import { expect, test } from "../../../../playwright/coverage-fixture" +import RooHero from "../RooHero" + +async function waitForAssetsAndRender(component: any) { + await component.evaluate(async () => { + await document.fonts.ready + const images = Array.from(document.querySelectorAll("img")) + await Promise.all( + images.map((img) => { + if (img.complete) return Promise.resolve() + return new Promise((resolve) => { + img.onload = resolve + img.onerror = resolve + }) + }), + ) + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + }) +} + +test("renders the welcome hero in the VS Code dark theme", async ({ mount }) => { + const component = await mount() + + await waitForAssetsAndRender(component) + + await expect(component).toHaveScreenshot("zoo-hero-dark.png") +}) diff --git a/webview-ui/src/components/welcome/__tests__/__screenshots__/zoo-hero-dark.png b/webview-ui/src/components/welcome/__tests__/__screenshots__/zoo-hero-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6f5e11eaf9e46aece8e6b9178b46fcb42d7b8532 GIT binary patch literal 20809 zcmX_oWmuJM)3pJT(%l`>-5{_jDZN3Yk(BOEL1NRTG}0yAo0bOY?%05obT{A0a~$vY z!#@gFoY%~nHEY%w`d&ruH7W_}vuDp!_XQ?K} zWPOsz2sB%C*`Xs}8V^*X%Nm;+8-F!6X}3slXiDiV>FJfuf62HPqb?{b8%CxMl&7qU zz>L62(qodSgN#zpb*n)rNp<9$fjy02q1!2()^>Il-Gr8Rk7VGNx zeFa_jlCakvWABGW4vDBCR7FKaLf%(L)n7)cs;czcd~E`evkD>5)&Dd#ua4lIr&}X# zZf>O150v1#FHnE^94?Ff+al!9$HT`brj`OXKxT4raS6d7uku}P^YfFI{)SBx7#1&u zfcGX|O+v!sWMiuMlPna=aZw5(c7A@|X=_;dai~9@@j4G1JUxVynT3Uvlr;4r_1{c+ z;`i@F)Zoz(;uARxf*()Ty2B$QBV|oQpKgw{dv`h_5%TEk?CjhgfcXFaR#Zrc_&7_a z-j+=lHVPe-1sAPI5^*~SC*;T$yZKk%rt$G3aqQgFqY~KI*r;{Z)1K_jRd;kq(Q*@k zGi#?|Wc*^;VFJOR6feOs4bDLkltMrr-@m;)T%bHXf=es+^xM}6XJS3iE-JDZ`6L=^Q)^*ua4KFDa5#K z8ywbqzz6?Ss$FXj?*QL}lxrat{JPN_7M5r-A){t4At8_5$sz%}Wpqr;FQ%qq?nl3& zbK!wHP<5%s7ONTdb==E?B@vGk6IWN)PoD@0#Md6a7!hPf=s>?qR$(8`+Au09DS4ky zeWItQhuP7Z(K_w!?nYqI;GdnJ+pM%M(Z!h#B;XSg8h1t}$$&SMe_dN!+Z#omQ)rH( zq_4l6!e{kU+{Y8%fkI)Mch*8PB!}ELHr#b{`y#qbzE<^Vo92QCIe2rikOJ#>Ut}qpIvF(Hs zXQH6ExL8L=$Dr9gHqHra`RNqY3D|W7?UtL@d!th)!AV7qjg57O6V}(%3?}pR4lGi} z;(#wt_hWN&)2{Wp2jQ#LOc{t?HoeB0qM}gbpt~+8uVl`u7U*kRHnPLzfa#kRuR8S3%;-d`s>#| z57%1^g;+n*#JnzsQU%h}(}RM7&d?)}-+7(r2k ze00Q7dV0cdGT{Ubv&pO<7hAjtuNlts6Td>3L{Uh<@8YOPOLzVh^HOSjdO=)9mG6m( zZZ0l=H#W?RW>=}e?JG0qK?+DCD0@nco$O8(D=RC5PejM4{e~a;>4Q-?xD8|KuQ;1CiXwl+%{_b0=+EcI~a0?1vbrTvSX|;nS?dj>cs?W*EDJj_l=kB;t zMAM5*jnf%NugJi_Fu1p-4Q+02jwBTj_j~YmcAkXh()O}Jq^RgDtgV%LQ*v?T6^DO{ z-ENQN;In9KY;9SMy2U+xE-X(^&x`$q#6TaM|Bx;ElX{SI1L567?_8Z*<-nNaV<2`=F<*tE;8ualZSJwqvz1 zHtmQbXt6*w!?G_^n%C(+WYx);na86}OprNa7{5Q!ET+hszqA;acsRz?quBzFZI9{M zV%&?-eDQF9+tk#QL+-lK7Yp);*bwdSpI^V;(2{{L?V!)g%fp*1$)FT3Y)Om7n*4@0;)I1P~%6CB?j#_8$a(x;ex{g9@SV zT9g%h3+gN>vI2-Ff%U|;`wP-tvVG<+!)bHP9@W?%GCxj)sbhd6!xjL@?smBRno`yg z{5rr%h!;U`bab@cd`&-89%K=kZUML_G!>j;j$E{?_jWI(-`?I{#6?#KCgmE-Oo^5; zglKMVPFC=#8h8VkeL4(&q==}f?7cAxF~Qw{FE=W`B~KT!%t6!1VX#*;l^sPv-SQ$N2z{@D6TUEvs|ZM z<(pDIE!;HHZKm}p@y&k5aWM|z_q!3J6)>tfjz$c`}gm&iQe7L zH6_JZQSSFQd~1$d!{oS(Uq*iJ|NA%4-yfQP3t+zye0~5$9^ny7(Xqccx)t^dSFr$; zk~!R7*xde}4{m$BKLo+AUuB|D^RiFL_X-{YP#R!LN(2H@QN+Sil}H0sv(0+0svEYJ z@p$(I;9AY!mu!7XppZkAl{O_abM=0tKLIcXR$_a*B(8+}?&EdFV^MK&wZz_Jk(%mH z(d%`}M{{m;Hgz0AP@AAH9e3yFwf~rp@xQ750uJc<#F^&Eq}|}Or4s-7{T9dy0zyKa z7m?S01-b~4Feyq(N}3(kgYPF|W8>oDULs+1@flSC&e776t{)U$DNGFF2#T!vMz)r7 z{yC@v@>3vZB6WmCL;%S<{&j(?YEHd5lJN-8pGbo0dxdlIJLa+#5`Q;<2iiqVZik*F zSaA<4tvSFyp`V6+g2`T;Xq`i6L6c53PzoQS!_tFq>T0;G>s*q2F}rnv+g=vSjN2M1CYCvpg@VZ+17Ldo*d!2u=g`3 zos{P}NVA7Te}Dg-$~`>!H?pw&{2sGO68$7@TK})TRip&x%Pn4MD5dg{f66KW_r2@U zk&!aqh>ji{4UzI_8?c}&0ree_I>h@Q{zNn7M`AO4d)O(+`1R`-_?BOq{m=XW)#>7s zmX-#*0y|pB=jMzt!yD(AdY?8P1%$+vKp^Ht{N=^LlA?FB`4~;AaTJAEnA&oM;kV<# zSXw#XyJNYUriu#AyAL$RH6m+q3}b&%jrajoUR$A6aNVEBKjrK=0AK)pCmh5leg6$p zobxAj^7Yw->JBQznvFu-8EDQuh2W6wjDMR-lagZ8{8$;$=E>=G1y|hd1UYX2IVhY&I2QJhKJ+U2g zH$OiK?p>cF?n4=unx-ZZZ*`gplShn^10cc{nV0Fz_!_WXFMW=$=LB^;|vTM?$+)-Pz1Z$+U3f%Ys1{<;m4R1qEwiGwS?|8DfEO2#!OyZTmW`WjCu55^Ag}K z7DA)PMJqY$X%ekXH8p2<*Qa3(U&Q|R2=j94r(4yB=C8Z^D%Uwf3r_j44o zy;+%)ABFx;3&5|&M5*s%1@QF5 z9fXDdU~cPONiD?3$IrsuFUx#xFWBan=H?W}pNt3U0hgD^j#Bef9oxj zd~k38Du=sUS^J)Z=b3qT7(V;3MlJ`AZ7lQT}WZFAa##k!{m6(+EQwhGw1nYy|1)VAEfaa-L4EKVH`Uu#x@thp<5i`^W6itzFY`Ib`Y;7Md7VP$s7O=5sN3oqy0x+b$L{uL? z>p$P#HkXuq9+gy8aGFIs&2EZDz)Hong>OV*pr<#)=px3cn~VK)2}yQif*a1dZLgXb z|AL$)lj$B(cf+s*K;pVM=y-;aY}YR>BudD~_QXV@Ql;_&MQ~&6!{?MNESOvQ%=!-i zJ_e4BU0mm>$mfl@%!;Lh_lfbsN~Nj-Tym1Fpd24R5)et|13%-GBX~2Lrq8Oyx}t(BOVSTnTEIIr?_YdAeePpi(F_%-_0=M{b|!-*bNgI2OmaoR*V zSmz}_ZZ!_Ls`1e;hMM#PQ0tMg{`~pVm(%&r7r@QiMdRUo>h`z7p^$6^9&tBw^X(7F z>jK^gG~^w)C(bEIw|l7?xs8p&92|Bx^A*Ex2Dm$bkL1$B6d#-~_vU!?e#yH7^u?=u z8^C4&aXyhvB4-+g_^jyt*8 z+Zf9M50YxgRx^K~A=u%G2?_rR(o@5cc8gQK##76~WP~ft$Ff1q1FTR^=OOf~`%ns> zC<(hOY^K&~rboAYN?RL2&Ms#O@E)y7Bg8|QueLZnL0C&mOLT{0D#5|O>gp1jlD=mI z(@EYx!{!A5mj1Q_Dz_Ki?qbxR!|Y{iYdgrim$V4lpg(`!rN&a2Fd(eu5~J`n0QM}^ zWI;V$ZlD^DQ)%0e;_Tw$z_7l)E^G2iOBQ|EH^Cb1^Xq2F9s zR>qHUwHD3+O5|QNwL2UU1hzHKh`@@sbZTHj)RnoEG$}SO-Vdnr3%IR?g+o<2(QwLV zYSA6pTM|-I@&iI1E-uLgo3`}GT3fvRz}odD1v-d;Gs)2vWP!b|2(S7T2p&fHvi@?A z0OY6gzdQyK*un+Ni2HN;cIZ+WjAWT+0Ig_y)dy6f;o`CE$*C!N&sr>%pRVDq0q++E zJgaF+W9`GAjV&yM-+drrWoM77(_I0|9{4y$Dp+z#u~$W0!xQaPIXhg0oXNto^OIbg zG-?_`HK2K}S2b@?}QA3`1Ol4(dN(#xY??eHjN=VFn zf5PV^HEWA5Qx!B+RC7`?_I?u?_HW<5an+_4241{cSz2BmCT4IB`dA|NTGQ9p_e%EM zXgX&tS6*ymOiN2k1Sq-OeLxSaXQp-};LcCHoHMFbFujx%#VDuhPf{ zZ;iG-fiDf(q`f`scuqn36oGjZT5fb9+gR?SW^!_JAt9kY=PMON(1E!hL1U302x}d= zpG8U74SID=1`!HW)-favf5YnR?8KOPNo}&_zL82m<3L-|uT)suifjn!0j0LcpDOj| zga1pU>`;;hgjhG;A9M>tLqm~5uc8XphiD1VfwB_y&Go;nw1@oWRRHqj>?l%6oyXs? zLi?^+H~2#}5yH$$nwoTqznkENy@Q};R`A;`i&kOZI$-k?^Tz3EXqb_GUM~X8fz?{z zuk}O&Uj@WCvrf2L;YSJ)*9eBN&-a*qS1+$o5H;5+d0#b9NAwYGz+_OVS_Pdpl?Jh~ zkp=u^Yn~5)P7o9a*FpFoE-tRRI4#EMTbPxVm0pRV^1E+3jDw+8Zz*auAPLApZ~Z_b z=86G*4S;{&z(6>^c9@AZI?$Rf)-0UH3Q&&~xxIbUZD+9`yGqxaA8R3Sf zBke6rBpO)|#}+OjBO^1bU{cx8a6MQ31)iU@f<-7utPmMGbNNK3!8Q5sB^95x_e%eSG@kV>@J&DKN8?4Kk! zO#T+A)Aihstxwm%fs36eT`Jf2W~U$&YfSi@3|D!$59&dldXLKLF1)Kcjs1Y7PYQSC ze%JWxR~S#k_RuvI@3ZAkGfhrg7&BSL#b~edijJ(LXv@FR#BYtZWF{%JQHu_2dci+z z8IQ{}0vRBRiIo*{J1VHO0y=oM$~vyE6YbwHF))Hh0xI8q4}5c>%E81$wGGwqa3XRt z{)FO06J-qVN-HNM!qrE*jEtaJNoz3p_jZgYGddvPl#Lg(@U!SYcUgX=Z`Y#8*pCX@ z!jaEtSXit8m~gr9)0i+ojjs;V(ZA6KQb&efFqN;GxFH(338Rit42_Hkv^uVJp_EMl zpkd}I)2!@B$=gB0L%(AE?pkj>$9=BIG4&xfs?PyUPSZGWjLOnfu*v;XTQz~!>++x2 z?_&}mI>`2@Co$%|?;lG_OI^4#1F&f4&!kfFAbV8OpF0MCB#|;56yv!wp2x(Rd=40} zXj%TyRNwG01u-$adc8H&vu09V7G#8qWI48YoRN`j1YI251k#K5od!thUrGX$kCb~Q z%xq$2sZ;D}HJbzCeV;WFv#D+^nup;O_%I17qhX_Fd}rxFw6?O6?qc3`{CK?1p5UK1 zw9aSo3KXurdQYtCXfj%es8VnR8T+pEMdWr2N|*N<$mIthDg{3NL}1u-8WTzLts$E4 zXGwZr7@!p=xEJBi0g60MfC$y;Jo$8Kl`&YKu!R-7M27{ zKr;QE=Oxl1FSzbE;#D8c%l(AX2-guv;tc%mU2g17PEHbn-}YL(xw@=h@#T?HlI{>o z%~FgaF9WI=3k&VvOef`$l$FdSjoC2Si0J5?)WW!?^rz% zlOn6Cqzm(%8w@w(ye_G*=Mj-l6U-UO*3=A3D170QJ;PGVN# zBpGPmyf^x=jc`rQW-E;uj*l8jTw^s{&CLM=cmz6l``ko&Bm2*<`H6LuNWbUCj|8>u z6x}5|#Jn^c9O*kC>0!dlMW7hFuymcRsB37j>o+Hr3Fq-31O!r-nXDQir#$}|BQ`=P>f7hEIKzPa%hc-q)v*?{o>3ij0_VR?Bhx`P4Iq_dVCJ=*} z1XuWGgsyK9%QY(l>Sa?bI2wNHCRkDDWBf)13^YyF^J%uwMFm$e2E8=Q zB1j})*c~^-*S6;7Alx_EN?HcVFDLLY+;g+wDgld*s?7padmQE_)q{wfd~!+Ox=3w=%P{CZ=;N z&AZ3vZXfzRHr-DgKR58b{tGl4AP7s#%tH~tbMe4p?Yp-5_*i&j)2JN`i&0*0RrKs0 zy8;%t?G7~Ti!k$`fUJ9`!{E0|b+$`4=Q9SX_c)B=oy)>=uHmRAyvDYXg*+G)l~YM_ z+9Y1^pAIh)rLbOu>Fxg}Z|P4%D{Z;a?zlCzqoUFj1Qb$(*R~}7n*w+LOi>8`JR=H^ zjm1VK6`ytt3JMevTg}hr41!Vp2Q)i=1*8#~sj-`6qQF9u_BC+{-#genE}KhEX2>To zp#LU|cx*=)&Zy{$u2I;;6MENlV*4{8v%T-CU+*}Hxu2kQB?Lnt`HaW20tqdH$X>)3 z>xgZS$Gwyyc(&R;yTu!yowKfV*5Vu`IiF7_Zr|2f0#IC6WPIO9(t-RKV~<2)ggO~0 z61IFiBJpS)&_0HB_jSq2&Q4CCfo;G063{Qes+=2D(*&{0+Q5J_T8L(p5WXq%z6^>< z4dZuOV?&-`dGn?tM1^)vqZpi>uu9wobFwhLLulN|iDR~X9DP`b#@q!!dT;MWb8kcj zt)uA-pNlypO7G8+2HYKtbc^jlx-X<_fNYAZUSf#97=H%EHYH0KQuy7!@qb#thD)}l zx*d=e;Ud=5`O}NIsh1tpC$iX-*=lw?nD@Yae<^LP6( zU>4!C2yLOC;Q~1d6(1MSDd!~}xKpryzA&tvTM)?K)TUY-TZV=hwIU0t4*K}Uswn={ z*3q#TVLChh13Jl79xw)+ru*tdtz;e?i}D8WOh%d|33G{aDvA($>_JxBI5?dMG=n zI;>LDaV?yxxU}s^!J^^%RM6+9#%$LM(m}42*Vwq&~^K(uuy~duDs-S!(7#{s9u??e+G~5_-a8b zxT&jU_$^Qb_W9t5;cj$x7D)e;M);*FZb#vwpQNz9sI@qAw~VIzG4w;1ox0ui0SeZ+ z;1;L$@IlIsu}~pXT$$#8`~B5=v{}N*h~I+~7$*7n_{c?(adqJ19PoZ_{O{ApkA`NY z%sA#)J~q~$IrF^12s~~7=*WJs#WAf!HF9%)5C1c+pDNOV?WwGaKAjoCA>zBH>BT-U zhP}TY@yl=HHoh;?>IBT0fI}Zl$I?c%kgJJM+)@mO7E^bXXJ%$5E?25d(N2`9Y)c33 zXdqY!>D9He-{6V+m%@=E~c%4 zn*eWum8IoTff;|tN+*#0=oSxR>~g~;u#87|UTe~1e* zq(5dG7#kZiFk1uCJ&~_m!!<@@XEGDI9_1Qw+L+g3;L4rPsf`h3`3N)=Fa^}rkEoUw zd&9)E`rVz~fMB290QH)v1viDMmE20sjK|9X_TC>^u0w1giE9%bFcC?=4jf zIxl&aLSfV)kPVj_UBFEOMK;Y}lYW$$NLNY`p!TTsm;^UvxCf$&Mpc_hKhFGcettd} znQ5r2kM1P5VzJmTS_a+V2HQW|kL0W7U9(a#;5E}&vp;JA_e;+j)J%qQ-n3jU4~&e| z2b1c^eDDLLdIac0NUvUDkPG8Im=FGC-3wZPL!3x^hr$Ib_s7UCzi%5LbDevM@|AhO zJA!JI?v^ht{#rzKepL(v8Gs5VsvFIVaE4qAAlWEwhV{%yCyn|dUO~7M08ehbo4of? zqAqIAT7q|?>_t`tMHa|9b$&6H?J^5{y|ua#AZI0!-Edw&owjGn8t6o~=>Kf$_o>Q{ ztZm!Niwl~Mf-7_!^3g7}o(I$RynlYm~ck25_iX?}cITKiX*4%o& z|KS}LuloTvhTp?!MnN==1I;3TD%)y+jV!84F9Uj3G2BGM1X7w+w(TvOmn8Ke6a-BX zqXYypaB+XRk~*3jT~@~4mEK6uT?R-FLa8VA>ym8<@-RHw?~eA>-EaSwJufqSZDKU0FK=QWbmMrR=r?~KY~=xMUoa-XhVke7#uYw7gN*4C2d z)vw4l>0T7}7%wA-jUF>)alc9e4O<(Vr8=7hFuQA3!pwp+78S~sX&V!adLSD1l*jRGp!Iv zka24v`aD^keI2}KQufQ;nY1D96PEKyUcKPwDSHg(GSS=ZH zMaJ)Yv3qF35^Nrh);M5kA$WUxJ7%RHPrz#t6b4Kqu91NvrkV3aE}9cAWQd1AnBL#7 zt%Esdt;&y78utP>K_a^z$%JHYc;qj~4vdgBp+)*$M0QvAD=_-D*e}uZ+;L~qte|>X zKXKm5Bilm*S9$F&;JC25=NoWHyUj^WPDV`pq>BmQ%k;U+HKdFC0PW5b6eU2S@{TcY zuMdjwZLOxTw|XZkuvwH>P;vh4d#NS9H@CP#^8oG!AoG=$3?iz8q0*4x988W*PuF=} z+Tx0G4g-y)!dJi65}%)pk?7Nnzv0Nx(0wQ6Lj{;X)9p0{+l#QG&m)keRm9tRb)nFH z{P^+7rR5-NU|`_mbAL8vfZfhZ5-UZ}W;R4(6|3K9dgtMeZL9p-0UQY;9sz+7j+kwC z972iKVJ;<@Pyp@UQM~ld2nsxNK zCljG+?RUxGcsq0RRn&;cNafmIeO)INm8ih~1?5*cwEU&0z8$Q>BVb55SZENVdz~q* ze6;UFjeSc^xxD2-m8r6+FFR;Ljv-qHiUg!eYJaEg4ireGuUQvOU=|LtdZFHKMDxQq zZ77S7$qNwbF;TcZB{GW3K9_pg*DRmP1k;Rsw@0%c9(-*-L|e2*@;OyWHwQ&_oS>?Z zxpjTA*EGzTORZdYQcwuxAhlpXZZ+4oQi)Ve<6=q5%c}f)5}Y_(E(7f^0tQ* zlZ4R;xEh$t)VuC$Z)sCTMQ%FMx41RxVKOn?SF9k-1^lLNRvAD7yM^f^-_$sG&-u_dDgrBcvQFF~#Pi>=Xfn^Mj{;zs^ zIXmbcaNi{K;6vzfY@f2T{~MMd2fDSK9Fif8aMDTX9J$eczWlCo@w&-2GVy>MrO(-u zuj;C@AJ%Y9J}g*YKcx~Va!2%EP|^nl85zWn{={wQ!?QL+v&0cYHC z2RxJ4%Wumz+>Oyp!pJaTJVSw5m-mF_A#Y&Ko6J+N?9QzT`8vu;S3r-STx?Xe4dQJ78uk}V31Yw68wkBr63!?DTJkdUIlo!5ZCK|y3>;>Z zWE6i`XQ&gpL}U_NFhk&uTnG+fW#7c2!QRD%40@9<3vjsVeO?XKF75(={8ftF&)RzM zhFG!&okUOD+TF>C$L|>V^`lH^%vDipNK@?93)*;-;#QF6B{jG*51?1Ny}4NfC{cS1 zH74D*71=9Qvu9ytjbK`YsW1`oC~jb@^vtAP0YlENWqlLY?zxv+v-iBohXYmT74(pfu^a* za6YT)qfhOafPo&n{V59+p_hk!p;mh#z_;Exzn@4$kr2NZgkw%awgUM3=s{BJ!XIx- zOUtzK5CJZ(v55)X{SP4@P~uuZ-87FDRw(|`92;NCKs&vXW*4`U&ZbkhdSh{2!mQqE zJVm7IO=h~NA7jr*W}6xphf`%SY5C?}o;whIV*iaHT8oSD00PHD8AF&WsAEOIpKW)7e8JVpAkNI&CqhCNuN|3cD{NzVj|Je zQDH=WsPG^p{Ac2T&FeJI2Zkb=SE-v93@{yaA_GCodI@?fOTc#z$$@=^a!ON-dy zyP;H&7ntVzfx7r4?r58~h8y=rB}1gV32iV9oo zPItOEFzkSm1hwpu+X+>PYe$X8yDbT9jN9J`_4uEx4Aqp@sjFPpfsy;08Q*3VY2eO; zvcQcM=G=$o@^$RAgi@pQK~zcC=vfBPcNJ_#It}T{!qV2*I%0(HEX`UgDTL65Y#9z`R{FY_Y$@BBNn8)(XbKJN@ zr19JfNc|RTe11!f&>HOzUmDuCw6wsi-;DFN=rYAaYhv*q|ju-Cmqk(F2rxgh^$ zCOn09PDY68B6t@l$sC3Y3_6r?Z^z^)@3;IO7+vrf6~pO}-TGC9%VLSi@P5#tTFp4! z8e;@eq1aejO8xziLA7^kBSU6F4vgr>RocA&k*X|nGTw=n3lSiGw>Vu|l-4HEIEqL@^dL%xsNY1Z57efWU=7u7GZ-_>6PMWbgQ5vBf- z>UANf(AUn}3DlHp|1wX2xhIgAcbB*y!UdH?gfvNO@8FnzZ*lbqVZfhQ4gA#+%|loI zz-6(H?)jJ3kwV}G7&gpmLee4>v*bDwt?nc27p%lGgx9*HID!!78LeIseBz@@WojUe zVuN2u)g^ZG%LqEx7foe4vuh+4)cXQq6m*V;+c3s4hd6}XFh4eC)rUw=(1p`0CSYYX z9t7vq71{5!|DP6c#%NvkYQEli=RZa#<_uAf4n-L?F(cbR?#)%K@84OOvb}ZKYzJ$P zEv>9d#}QZ1Tt%8AxDfr&HBXLSy3=wexX(E2wfWvZx!!09QsW~uFO*Qsq2U(KYu*yW zDq9Tt9ByB^F}rbBc>zIgJvkIh?NN^`8wlHZZ`;Ql@KB3r$mGum-Z+zQCq4H1-Srl! z73dD;%6<8ullw+nYN&Vb@X+n(-J|VG>&dVgxf!Dab|9MDuwxd4CG%BgYW>Bf{;2D5 z8-ya12d#Zyg}6ID+`2GmWnqk~}M0S7P1j{TR5EnsLQ@49wSJV2c-7RFq6 zV(T7nXcLt8;Jj}IyK4Of#n93MnUm>yW37+ zPA9Y275DiMd=@8Fi$_Ja4nB=->+62Djm=y=6ibFxx!TWCQ&*Rwd9T49%9NNM7Qn(=ek~@4UJ1k2H&Ab0@lLSfr(E#&KcaxJ*FcIOc z&&>^Va!s%QEWc9#HXl^k64uC;6R*VQTa;gQ5DPnGLJ;J=YxpDH2D^nOyR0sPnK+gM zTc-!LQIBrgTLS}1J4lTY&m9K5M=Rve=OvgMRak`f_tsg@jZaSplV*;+zG|HECcxDe zVl4aeVO{wofUO!u$W|cpYm#mUYTSxDVWXn>mb9jXF@C>JjH41IDn&U>W~uxUusxjs zi+XSED1x@?fMInZVje#ZI^)OpDkAf{B{i8Uud*goAgQ-1YI53Zxu)5w(&#u&LAZ*O z9ijX<#vl;yT*e&TwP;29V~`t?Jc@(fHBdv9A#X_bc04`i>^CQ;aSTy5;~6TE+=u`XZ*$wtfM|8rO?CfAJjQuJ7H_-npIaI z8$Gr2J+H3EmE5L44H+FZlVA~Y%WIWjVS7)7I>f|)KOW6!g|R0{UwC3_q&vt-OP<$f z)P4^+^--TdjI$D@6%JUEKDqjev2{*u1Jpza<#0&t@!HFmFUQKLQC^iIHxgGz{c)3| zC9!!cz|q0dwf8!+VESegezc|%EO8=fMB**{1?mN{vN&I^*x==<$sAK*V535+KJ*Bb z_K_#92hFeRDlJ$sfc~KJf8CMtKJ?{Oh4?x^!GVDRaur8-k2G|B+}6t0lXoK00JJ0@ zl@5d1{?J6SL7%70Dj!&i zl7~%6@5T_|c0x0zeh!eDmX@}M=jt6Ji%)?Fd&1DTO1u7h^^>;D7aPuJt+Cby`8e;t zcwH{JdCVnE)qQq6UX!sZF9(C~o{}U7I_|CJ>WGAopFYWp5XxFa0)3i8$r3RObeE~iRlQYa~8Gq<>yWzA6j0=R|ZgAsIw!UG?rI>N z)frXET!J%7kAz~S&8C}$ZjiTZE^M^jQICm_MQDX1C83)Eu79}Pa?H*%(0xXqJgCGi z1BDQoNOeTuzn<-w!Z4$bW{dE|!8+{fTZyj7jvfF?*(_*VNRs8Dni2C1(;) z^AF3(&pZf!?Mnq*OaD1~HG)>H3p+hMO$1~6cLPI*hbBgUjDQ>{-uQNTEw{I~G|Kz* za_C;QIZC1T8!n=~hCYF=vbg4F@fST43}1WRo$g?@C$XdZY|&`q9&}Rv-N8uwmqXpI zCATMk{0GoRl|~V78dMzYHs#`veS!Wy4*rxkKo8KZF+7VYv0;j^?^%w zKcrzDz%Ibg5{er)+M^Z*!|i}-+AADWB4LnmmK7prMJ(`DzlSx*;MV^;03OQzO`J)J zD`XtmbSyb(s?_*+=`R_DONK+E)VxATi=c0p6Zz`p1vFiZdeY@O8`8gP=Fw|=Y1T|i z*WdGbK^-ACzsNrF8nVM8y4f>muLTK1qQ5kMy7pv`}&qq6d`_8)2l&P}nj$fVYw}!8ln^J0;eX}?A zZ@~Nzf*=!fh35J1nBA8^^GXnbyvCjhb}1X>$kOTsRC<1-!C8rkP5HQEu-+l(jpwG6 zu)7hsKh$LZx>Vc|o_ih94^~dd?rg1M6b{$uNCL+;HF{0xjnZD3R=y_XkKq`}Hy!L# zwHNlLdExA6smn7L@b3k4E54Uv8o5m%J<@y9+;pf`$68+yHBIo6I^H(#kxcJmZHN>v zS5$DyHJS`LSsX{xFUOH_sgVGgeKq+;RDp-^?F%pv#vr6}X%C_Fg+f2qtdnP^uxIA# ziaBEyhmT`xH7+c~)c7aTynEu|T^7gFKlzxv23bPc)r@l=@f>1M^a5jJW6kv_*YHTP z!1_aM?KVk3@wzF-}pu7%d~4z+cr%Sc$Cd*Bojc+;blnN?(zI$rEx$0-lf{eU50AKcyDfo)b=xhOj4DUS%o#@_}Xd&;uk3Ss}V zER8c?3uNc4Z3nGA-~96{+g)vyvQp(&?^6E|4=3{=y)08k!$nO@$v*}T+#bdlYfrPC zraf(Y`Im5p^3`%Aa)+5%H+i7(#6_A;L-qyDH`v_JBKy*d z_Cud8wU6k!hf2iYJhD8LBV60GMzU4(9lA2i@URK^*TMLE+|#tICcm*Uh2#EgXQD@9 zdvJ`XkYNYyxyxdbLCRk`^xbR~DKWO>oa%JYno4fz2B)A&o%ErX(Yy z<4Ph?k%mG}rKbm7)?eV@PB6scd09l7Kgq*~D{YVa^((@D1c%|S3yT+LcH0s|S69xZ zFu}8Y58-pAeY#XB^9q~bt)}&yXmLmGYy&}R`;Mi>MQ^a4Ne3BB<+u3*8it3!l;Gf3 zzfLvg;?c3K)PR=F>))K+!BCTjn;mTAh`tsTB2xsOXC!C(#f0d)NvvwrnYlSFUEK&5 zpNBgk)vVgzh_vDCCrE=Fj0*1ii|k`Mzsk$oz-C>|OLOfj=V@*)Z&__}l{m^8mUmd0 zR6F*6ilv!!f%x@-R^g6@sY49-r4CLvG5q@-_PdXy?f{AA(uv32M3CaIV=x*}P*O%) zA-yJ~()ljOSt{7Bt~~nt(Wue53@aUZ)T-nLu$v=s1Z=R$yr}XWms}q;LIQ~c7#Jwi zO8&|I?m*nSo!U*`L3Ybi4q;|5v@~ZX@>9rL-{g2nt}v|e%UcNt4hCPZ4AUr25@9a4 zx^LHjkZ0799M7mKjO{=|LDH7t_b|6fL_?*YV6r)$r}$9j&mQ+O0Rw)qyd9@-B9IwN z_Yn62o*zG)nyu?wyT$dSy!#2vUayusDpIqX8!sWZ_ot(zDD;3q!Tx&R^t6WKb$RQJ zCVBy1&kY|l1>>$5Py=Miz?v8_eQ(VVdWE8z0el5N|3>0&m_d3 zkssahK59oo$eQ0|z@}>!oSU2sv{XyUyfBY@q5MHr6*FJFFZI;X6YSARQryT+x9f%Y z4rBu?ZB^<{Z16g*qY=&nC>4Pa47ad;U9JSaMVpI3aA z)E?BeYri%Q`adne%1Tkia2joveqQ2Wp|!OYEHNsF_4B^BiL)i7y*5Am_PCYaN(X@{ z$lC#c2Rjaams_>|oVLK5h_HS3Z^G0^FeMqxFbaa`vu5QR;$}@iw|ACN^TI$DtP)EO zDie2Js{z4&3G{4M&qKT5UwV{RD0h?|0>i=L4%jIhQ_s~)*&JYhukh+|YJA)%54U z(=QUg&sc8z>Q4od)Hmy>Kr(qd$7R{P$2 zt^=eZImiP-HgFL)``+Jx%@r^oR-@PQ?V$&NZkX6tzFm()@6dgh^1FwHG?q(AMaAfc z5ru5alJ8#I5t;z8KtF3(ymyAVO~!}+{sZ3T=R7)@y zt67Bsm*p#9dNTZ6kQ>(J-vaY{^o6L3adN&DE+7w&ligc0`SfW9=)rMyRFlpL?0WAK z(@ehpLRKil_LX)80iayQmafa%7LUp^Cmh-k@$uCO_@Yr!QAgOc&PZSl9zTvQo`^nw zZOtg)L%g&30`Sm7_@(etM{SmZP?loA^7ePGdZ5aRidy?|zV=wVc*J`i!vE9DsfPo6 zo04Nc1%}}DeB@x;x|*7pdWKR;2x3)3!<;tceSCS`t2{1u-m1a<33!2hy}PnEcH(-UK_8LB74eO2BrpF9Y$1RojIU z&au=OjvfUGNp=PH@^v0-|H?Ol?hzBP^Z^;SVdnE$1H6HmPC;CSaW%#w&e~DYNg}5d zuh!l~oP7h*V`=j_|Jt1*%g5(@7oT_r#OOK+hah>Y`9c53VvQx3gK=YW_TOm=b>d-= zM>_#JUWcVXBeAw-C1-pFy3?6A%uGxbt~>dSE=Q}999+CMPioNaXC8<;B=GP6ASrKN zUkysKY07v>39D^x5mCx^PEE~ZVE#tty0_Y0kH653tk#n3Y?@zCU3)qr5hXpY z<)W>?xSFQv`9R=s=CTvj*3cDIexao?TBn1{>|fKBBFHwSg8xv0(Z{wlHs%inw46fF zo@%p)9Rz71zd^I?kM1rUEgZI7T+02!clwLI+uR0}!%O0yDi>R@r4XKF)U)d}yE}aR zh>wwiGYuS=9len*uC6ChAmTAj%Ji5+E~05T@86dk0AZ1H2Bk$8bOn=aSNiUR9+uPr zs@WH5xpJ!1XggUV(FB@?$XTfAtjdkmAqd~l&Oz8S_~Dm=9b3^WU^}eE2*r|QfTtp= z3FK6(CMc`^zc#KtAf|1Nr^23!#>k@+O*A{pKDZ6Wm zV&r%vWK`mokZwbS(F3`jM`*_BF%{)b3WuDc``ga_^ZvVM&Dwi?>sxDm>-&DcUmkzO zlzVHRHa8EIrF?5B$i}gdRtC1@W~~hNZHt9f*Y)4LwCTvO999fCj*C43*@Qt)tCrRE^Gm9oQ9JOQXM|J(+!Dsww$3ACi@(U$Hn-DM>ueqtUm#?35#p8`jkm+OrZ(W`|{nuh1Y^R?tjvG(^5UwKzn-uzMSLO zqxGbzi6M}g%{07>07j#<7KGzndWbg8`k%->dUW=%U7zou@61Uevs--rI5;V4K|VB`a2pFYaKav#}Q~ zWBoDy`RLy-xFNwiolDK4F9K=v6Jj3_tpNj-=4(0*|5o_PPm+J8ivKgJ7n5C*u&vbd zybTNtP_Bl_E=q-GcQrOPrVOgN!<(Ph<<+fAyuA-g6d3N5B8loWebjHTA4VJY*DX zF@ER!@*FQvA$)xsR)!@RD!WhyPvXNjodl9IvBvP{RU-9DvWV+#feaBsHf!ja`oOiY}<#g1m*bI(>rc0N4u>p_1w?c&9cARBGncmy-Y>EmO+!_w)0vNWbfbUkWfukms;Ch{ym*FUYl+kb)xf z*3UUezcJ-Lx)1d(7X(sZb9xfe+b}GBza5AozqKzfN9^6uNRazzWf(kpk#-Ogtvh#C z2L=;su;%%JP5|V_eINV^#MeZUCP?&JGSk(o3(5)1a&?U3nZ2vgBem0L_ipTs zmJBv{OlmY4AD{g71C*8|Z!w>0z)I?D5r9vBHRwk$KAn8$$k!~Kys)4hMU0soq58fg zAOQDL9W}#24h!`n=IoUU2Rt@y>O$HiEAq-2Odv1QXAn%#FQ91GnP@Z`NE+}dV$I%3 zm9Fa}BEEeecwc7%SaI&`>9iyR58K=Og;SG5LYDm9+3qh3yWt`;eD%n@{Nft!_5H(9 zZb|_R3f~!o*|rj>Op9dGlZ|#V}43*Q}%0A%^GVq=aM^v@2ZP>b5!`ICA983!VWb0i#%> z1C(T)4v;qp(rqcC)xdZ@x7Wd}prqtMnWiQ zF7#58eG7n}0dfXxgN-okxJ)1C)~L@}Usf{)+Bd0knrc+nGdDMq2A#AAL6GoLM^Ic` zd41_anr)md<9s=gkmnGisJVw22s#4M=)*b|EoaV*pHpDtS z@bPR+#jpiaXOYvcB9E4llbSkgIVQkVDy%BN%D}qi`GXgFMUcT(Ov&^KIS6cPTYba7 z6!1c_b-M?^kzm3s%8TE=ee)Y#k`@{LCMT_|8fpoKQwplX3pj**yIfrax-pc4cso^v zRunNuhb2mSaNOR;)wOv+AcSfLf1En(lL3JoE{ZoA{f?AYUAX=5XLlN+Exp4x<@dv#cq$V{w)M!~NkKwzlX8fCI60j5-f z%N|yhNoDwRu1gYQTSE)grPF4KsGg})n^T5f<1-f*I~X&HO^|xpl9Z&97eWE9HZ~Be zb(CYzM6)U=o$*aKMn{V-ZGrxqz$06kjgoWHfTps}1gp%GAO}4ZTkDLXU^JRIEc*l+?m3$4}?0P6h>3&~&>QuDzuE-YzMC5LI%QX|ZT*SCH`fdSU z1wZchu{-=&@)n*P5gCb7C%3-~3wPjiDddV2VeRsDLO`pakM!eeW}|Zp>T~9to|hpv zc2s~=QnSD4sy@j{rJd{u5pJf}?_8Q-y{h0?4%V2+6nl>!X!je6N`#X>VPOdIizAsh~QZ|3m?9sDdW50oZ#{R_hVPVas z!IcP{qA;rHc0DzcY$L~w8|R;0a{hc<_}{%u=Hfo8L`?tS4$x(ru)WUlma8}5*r@jT zWyOla@TdPh>jWej^kut|xQsx%!WiOu2An8+9D!hA+clku>wM5B&xGY)VhDGTa}|rl zII1S}J%PU1PiQ@n6LkdcZ{F6n3|#kBG65l7VxgLqh&TXqQVyZDJyj2*At9(>v;&QA zmLdO?_^}B40S#Av1Pu#_r$i3U{jrM)%%wtc8ypyg$7Ms-mX-;BeL9Dx0_=c{;1_P? zXTST_E)CX!p|fbE6@>Zz0(iaj)WREF*PUB1C@FS*#w0j^!1i3V+=r0rcA~y++-iLO z5E2aYw{;&vkGkNvQ4+pKK*s+U6@pCo@gyx;4ip9g{S=&{>jWf literal 0 HcmV?d00001 diff --git a/webview-ui/tsconfig.json b/webview-ui/tsconfig.json index 14b36b878b..e71653d66e 100644 --- a/webview-ui/tsconfig.json +++ b/webview-ui/tsconfig.json @@ -24,5 +24,5 @@ "@roo/*": ["../src/shared/*"] } }, - "include": ["src", "../src/shared", "vitest.setup.ts"] + "include": ["src", "playwright", "playwright-ct.config.ts", "../src/shared", "vitest.setup.ts"] } diff --git a/webview-ui/vitest.config.ts b/webview-ui/vitest.config.ts index ea1beea51d..9b43616c1d 100644 --- a/webview-ui/vitest.config.ts +++ b/webview-ui/vitest.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ reporters, silent, environment: "jsdom", - include: ["src/**/*.spec.ts", "src/**/*.spec.tsx"], + include: ["src/**/*.spec.{ts,tsx}", "src/**/*.test.{ts,tsx}"], onConsoleLog, maxWorkers: isCI ? 1 : undefined, testTimeout: isCI ? 15000 : 5000, @@ -33,6 +33,8 @@ export default defineConfig({ "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", + "**/*.visual.ts", + "**/*.visual.tsx", "**/vitest.setup.ts", "**/vitest.config.ts", "**/vite.config.ts", From 772e716fbe60c200f029f948b4d9cb8c78d42f9a Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:21:21 +0300 Subject: [PATCH 65/89] refactor(api): use canonical provider identifiers (#1012) * refactor(api): use canonical provider identifiers * test(api): cover canonical provider routing * test(api): enforce exhaustive provider routing * test(api): avoid concrete Vertex model IDs --- src/api/__tests__/index.spec.ts | 146 +++++++++++++++++++++++++++++--- src/api/index.ts | 76 +++++++++-------- 2 files changed, 177 insertions(+), 45 deletions(-) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index b86e73dac6..3617c3cd6d 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -9,21 +9,147 @@ vitest.mock("vscode", () => ({ }, })) -import type { ProviderSettings } from "@roo-code/types" +// Handler constructors can require credentials or initialize SDK clients. Replace them +// with inert classes so these tests exercise only the factory's routing behavior. +vitest.mock("../providers", async () => { + const providers = await vitest.importActual>("../providers") + + return Object.fromEntries(Object.keys(providers).map((name) => [name, class {}])) +}) + +vitest.mock("../providers/native-ollama", () => ({ + NativeOllamaHandler: class {}, +})) + +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderName, + type ProviderNameWithRetired, +} from "@roo-code/types" import { buildApiHandler } from "../index" -import { KenariHandler } from "../providers/kenari" +import { + AnthropicHandler, + AnthropicVertexHandler, + AwsBedrockHandler, + BasetenHandler, + DeepSeekHandler, + FakeAIHandler, + FireworksHandler, + FriendliHandler, + GeminiHandler, + KenariHandler, + KimiCodeHandler, + LiteLLMHandler, + LmStudioHandler, + MiniMaxHandler, + MimoHandler, + MistralHandler, + MoonshotHandler, + OpenAiCodexHandler, + OpenAiHandler, + OpenAiNativeHandler, + OpencodeGoHandler, + OpenRouterHandler, + PoeHandler, + QwenCodeHandler, + RequestyHandler, + SambaNovaHandler, + UnboundHandler, + VercelAiGatewayHandler, + VertexHandler, + VsCodeLmHandler, + XAIHandler, + ZAiHandler, + ZooGatewayHandler, +} from "../providers" +import { NativeOllamaHandler } from "../providers/native-ollama" + +type HandlerConstructor = new (...args: never[]) => object + +const expectedHandlers = { + [providerIdentifiers.anthropic]: AnthropicHandler, + [providerIdentifiers.openrouter]: OpenRouterHandler, + [providerIdentifiers.bedrock]: AwsBedrockHandler, + [providerIdentifiers.openai]: OpenAiHandler, + [providerIdentifiers.ollama]: NativeOllamaHandler, + [providerIdentifiers.lmstudio]: LmStudioHandler, + [providerIdentifiers.gemini]: GeminiHandler, + // Gemini CLI currently relies on the factory's default Anthropic handler. + [providerIdentifiers.geminiCli]: AnthropicHandler, + [providerIdentifiers.openaiCodex]: OpenAiCodexHandler, + [providerIdentifiers.openaiNative]: OpenAiNativeHandler, + [providerIdentifiers.deepseek]: DeepSeekHandler, + [providerIdentifiers.qwenCode]: QwenCodeHandler, + [providerIdentifiers.moonshot]: MoonshotHandler, + [providerIdentifiers.kimiCode]: KimiCodeHandler, + [providerIdentifiers.vscodeLm]: VsCodeLmHandler, + [providerIdentifiers.mistral]: MistralHandler, + [providerIdentifiers.requesty]: RequestyHandler, + [providerIdentifiers.unbound]: UnboundHandler, + [providerIdentifiers.fakeAi]: FakeAIHandler, + [providerIdentifiers.xai]: XAIHandler, + [providerIdentifiers.litellm]: LiteLLMHandler, + [providerIdentifiers.sambanova]: SambaNovaHandler, + [providerIdentifiers.mimo]: MimoHandler, + [providerIdentifiers.zai]: ZAiHandler, + [providerIdentifiers.fireworks]: FireworksHandler, + [providerIdentifiers.friendli]: FriendliHandler, + [providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler, + [providerIdentifiers.opencodeGo]: OpencodeGoHandler, + [providerIdentifiers.kenari]: KenariHandler, + [providerIdentifiers.zooGateway]: ZooGatewayHandler, + [providerIdentifiers.minimax]: MiniMaxHandler, + [providerIdentifiers.baseten]: BasetenHandler, + [providerIdentifiers.poe]: PoeHandler, +} satisfies Record, HandlerConstructor> + +const expectedHandlerEntries = Object.entries(expectedHandlers) as Array< + [Exclude, HandlerConstructor] +> describe("buildApiHandler", () => { - it("returns a KenariHandler for the kenari provider", () => { - const configuration: ProviderSettings = { - apiProvider: "kenari", - kenariApiKey: "test-key", - kenariModelId: "glm-5-2", - } + it.each(expectedHandlerEntries)("returns the expected handler for %s", (apiProvider, Handler) => { + const handler = buildApiHandler({ apiProvider }) + + expect(handler).toBeInstanceOf(Handler) + }) + + it.each([ + ["an unspecified model", undefined, VertexHandler], + ["non-Claude models", "non-claude-test-model", VertexHandler], + ["Claude models", "claude-test-model", AnthropicVertexHandler], + ] as const)("returns the expected Vertex handler for %s", (_description, apiModelId, Handler) => { + const handler = buildApiHandler({ + apiProvider: providerIdentifiers.vertex, + apiModelId, + }) + + expect(handler).toBeInstanceOf(Handler) + }) + + it("preserves the dedicated removal error for the retired Roo provider", () => { + expect(() => + buildApiHandler({ + apiProvider: retiredProviderIdentifiers.roo, + }), + ).toThrow("Roo Code Router has been removed") + }) + + it("rejects other retired providers", () => { + expect(() => + buildApiHandler({ + apiProvider: retiredProviderIdentifiers.cerebras, + }), + ).toThrow("this provider is no longer supported") + }) - const handler = buildApiHandler(configuration) + it("falls back to Anthropic for an unsupported provider value", () => { + const handler = buildApiHandler({ + apiProvider: "unsupported-provider" as ProviderNameWithRetired, + }) - expect(handler).toBeInstanceOf(KenariHandler) + expect(handler).toBeInstanceOf(AnthropicHandler) }) }) diff --git a/src/api/index.ts b/src/api/index.ts index fccbf48867..c9992c9c39 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,13 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types" +import { + isRetiredProvider, + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderSettings, + type ModelInfo, +} from "@roo-code/types" import { getRouterRemovalMessage } from "../core/config/routerRemoval" import { ApiStream } from "./transform/stream" @@ -140,7 +146,7 @@ export interface ApiHandler { export function buildApiHandler(configuration: ProviderSettings): ApiHandler { const { apiProvider, ...options } = configuration - if (apiProvider === "roo") { + if (apiProvider === retiredProviderIdentifiers.roo) { throw new Error(getRouterRemovalMessage()) } @@ -151,73 +157,73 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { } switch (apiProvider) { - case "anthropic": + case providerIdentifiers.anthropic: return new AnthropicHandler(options) - case "openrouter": + case providerIdentifiers.openrouter: return new OpenRouterHandler(options) - case "bedrock": + case providerIdentifiers.bedrock: return new AwsBedrockHandler(options) - case "vertex": + case providerIdentifiers.vertex: return options.apiModelId?.startsWith("claude") ? new AnthropicVertexHandler(options) : new VertexHandler(options) - case "openai": + case providerIdentifiers.openai: return new OpenAiHandler(options) - case "ollama": + case providerIdentifiers.ollama: return new NativeOllamaHandler(options) - case "lmstudio": + case providerIdentifiers.lmstudio: return new LmStudioHandler(options) - case "gemini": + case providerIdentifiers.gemini: return new GeminiHandler(options) - case "openai-codex": + case providerIdentifiers.openaiCodex: return new OpenAiCodexHandler(options) - case "openai-native": + case providerIdentifiers.openaiNative: return new OpenAiNativeHandler(options) - case "deepseek": + case providerIdentifiers.deepseek: return new DeepSeekHandler(options) - case "qwen-code": + case providerIdentifiers.qwenCode: return new QwenCodeHandler(options) - case "moonshot": + case providerIdentifiers.moonshot: return new MoonshotHandler(options) - case "kimi-code": + case providerIdentifiers.kimiCode: return new KimiCodeHandler(options) - case "vscode-lm": + case providerIdentifiers.vscodeLm: return new VsCodeLmHandler(options) - case "mistral": + case providerIdentifiers.mistral: return new MistralHandler(options) - case "requesty": + case providerIdentifiers.requesty: return new RequestyHandler(options) - case "unbound": + case providerIdentifiers.unbound: return new UnboundHandler(options) - case "fake-ai": + case providerIdentifiers.fakeAi: return new FakeAIHandler(options) - case "xai": + case providerIdentifiers.xai: return new XAIHandler(options) - case "litellm": + case providerIdentifiers.litellm: return new LiteLLMHandler(options) - case "sambanova": + case providerIdentifiers.sambanova: return new SambaNovaHandler(options) - case "mimo": + case providerIdentifiers.mimo: return new MimoHandler(options) - case "zai": + case providerIdentifiers.zai: return new ZAiHandler(options) - case "fireworks": + case providerIdentifiers.fireworks: return new FireworksHandler(options) - case "friendli": + case providerIdentifiers.friendli: return new FriendliHandler(options) - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: return new VercelAiGatewayHandler(options) - case "opencode-go": + case providerIdentifiers.opencodeGo: return new OpencodeGoHandler(options) - case "kenari": + case providerIdentifiers.kenari: return new KenariHandler(options) - case "zoo-gateway": + case providerIdentifiers.zooGateway: return new ZooGatewayHandler(options) - case "minimax": + case providerIdentifiers.minimax: return new MiniMaxHandler(options) - case "baseten": + case providerIdentifiers.baseten: return new BasetenHandler(options) - case "poe": + case providerIdentifiers.poe: return new PoeHandler(options) default: return new AnthropicHandler(options) From c52f1183935cc49e527c51965f2989d729751618 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:50:58 +0300 Subject: [PATCH 66/89] refactor(types): use canonical identifiers for default models (#991) * refactor(types): use canonical identifiers for default models * test(types): cover international ZAI default * refactor(types): name absent provider default * test(types): cover defaults preserved during rebase * test(types): document identical Zai defaults --- .../__tests__/provider-default-model.test.ts | 63 +++++++++++++++ packages/types/src/providers/index.ts | 76 ++++++++++--------- 2 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 packages/types/src/__tests__/provider-default-model.test.ts diff --git a/packages/types/src/__tests__/provider-default-model.test.ts b/packages/types/src/__tests__/provider-default-model.test.ts new file mode 100644 index 0000000000..33ba019253 --- /dev/null +++ b/packages/types/src/__tests__/provider-default-model.test.ts @@ -0,0 +1,63 @@ +import type { ProviderName } from "../provider-settings.js" + +vi.mock("../provider-identifiers.js", async (importOriginal) => { + const actual = await importOriginal() + + return { + ...actual, + providerIdentifiers: { + ...actual.providerIdentifiers, + openrouter: "canonical-openrouter-test-value", + }, + } +}) + +import { providerIdentifiers } from "../provider-identifiers.js" +import { + anthropicDefaultModelId, + getProviderDefaultModelId, + internationalZAiDefaultModelId, + kimiCodeDefaultModelId, + mainlandZAiDefaultModelId, + openRouterDefaultModelId, + vscodeLlmDefaultModelId, + zooGatewayDefaultModelId, +} from "../providers/index.js" + +describe("getProviderDefaultModelId", () => { + it("selects a static default through the canonical provider identifier", () => { + expect(getProviderDefaultModelId(providerIdentifiers.openrouter as ProviderName)).toBe(openRouterDefaultModelId) + }) + + it("triangulates static selection with another provider category", () => { + expect(getProviderDefaultModelId(providerIdentifiers.vscodeLm)).toBe(vscodeLlmDefaultModelId) + }) + + it.each([ + [providerIdentifiers.kimiCode, kimiCodeDefaultModelId], + [providerIdentifiers.zooGateway, zooGatewayDefaultModelId], + ])("preserves the %s default added on main", (provider, expectedModelId) => { + expect(getProviderDefaultModelId(provider)).toBe(expectedModelId) + }) + + it("preserves region-dependent defaults", () => { + // These defaults currently share the same model ID, so the assertions document + // both branches but cannot detect swapped ternary arms until the IDs diverge. + expect(getProviderDefaultModelId(providerIdentifiers.zai, { isChina: true })).toBe(mainlandZAiDefaultModelId) + expect(getProviderDefaultModelId(providerIdentifiers.zai)).toBe(internationalZAiDefaultModelId) + }) + + it.each([providerIdentifiers.openai, providerIdentifiers.ollama, providerIdentifiers.lmstudio])( + "returns an empty default for custom or locally selected models from %s", + (provider) => { + expect(getProviderDefaultModelId(provider)).toBe("") + }, + ) + + it.each([providerIdentifiers.anthropic, providerIdentifiers.geminiCli, providerIdentifiers.fakeAi])( + "preserves the Anthropic fallback for %s", + (provider) => { + expect(getProviderDefaultModelId(provider)).toBe(anthropicDefaultModelId) + }, + ) +}) diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 9a78de11ac..5f7d1afda4 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -62,6 +62,9 @@ import { zooGatewayDefaultModelId } from "./zoo-gateway.js" // Import the ProviderName type from provider-settings to avoid duplication import type { ProviderName } from "../provider-settings.js" +import { providerIdentifiers } from "../provider-identifiers.js" + +const NO_DEFAULT_MODEL_ID = "" /** * Get the default model ID for a given provider. @@ -73,71 +76,70 @@ export function getProviderDefaultModelId( options: { isChina?: boolean } = { isChina: false }, ): string { switch (provider) { - case "openrouter": + case providerIdentifiers.openrouter: return openRouterDefaultModelId - case "requesty": + case providerIdentifiers.requesty: return requestyDefaultModelId - case "litellm": + case providerIdentifiers.litellm: return litellmDefaultModelId - case "xai": + case providerIdentifiers.xai: return xaiDefaultModelId - case "baseten": + case providerIdentifiers.baseten: return basetenDefaultModelId - case "bedrock": + case providerIdentifiers.bedrock: return bedrockDefaultModelId - case "vertex": + case providerIdentifiers.vertex: return vertexDefaultModelId - case "gemini": + case providerIdentifiers.gemini: return geminiDefaultModelId - case "deepseek": + case providerIdentifiers.deepseek: return deepSeekDefaultModelId - case "moonshot": + case providerIdentifiers.moonshot: return moonshotDefaultModelId - case "minimax": + case providerIdentifiers.minimax: return minimaxDefaultModelId - case "mimo": + case providerIdentifiers.mimo: return mimoDefaultModelId - case "zai": + case providerIdentifiers.zai: return options?.isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId - case "openai-native": + case providerIdentifiers.openaiNative: + // TODO(#992): Replace this stale fallback with openAiNativeDefaultModelId. return "gpt-4o" // Based on openai-native patterns - case "openai-codex": + case providerIdentifiers.openaiCodex: return openAiCodexDefaultModelId - case "mistral": + case providerIdentifiers.mistral: return mistralDefaultModelId - case "openai": - return "" // OpenAI provider uses custom model configuration - case "ollama": - return "" // Ollama uses dynamic model selection - case "lmstudio": - return "" // LMStudio uses dynamic model selection - case "vscode-lm": + case providerIdentifiers.openai: + case providerIdentifiers.ollama: + case providerIdentifiers.lmstudio: + return NO_DEFAULT_MODEL_ID + case providerIdentifiers.vscodeLm: return vscodeLlmDefaultModelId - case "sambanova": + case providerIdentifiers.sambanova: return sambaNovaDefaultModelId - case "fireworks": + case providerIdentifiers.fireworks: return fireworksDefaultModelId - case "friendli": + case providerIdentifiers.friendli: return friendliDefaultModelId - case "qwen-code": + case providerIdentifiers.qwenCode: return qwenCodeDefaultModelId - case "poe": + case providerIdentifiers.poe: return poeDefaultModelId - case "unbound": + case providerIdentifiers.unbound: return unboundDefaultModelId - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: return vercelAiGatewayDefaultModelId - case "opencode-go": + case providerIdentifiers.opencodeGo: return opencodeGoDefaultModelId - case "kenari": + case providerIdentifiers.kenari: return kenariDefaultModelId - case "kimi-code": + case providerIdentifiers.kimiCode: return kimiCodeDefaultModelId - case "zoo-gateway": + case providerIdentifiers.zooGateway: return zooGatewayDefaultModelId - case "anthropic": - case "gemini-cli": - case "fake-ai": + case providerIdentifiers.anthropic: + case providerIdentifiers.geminiCli: + case providerIdentifiers.fakeAi: default: return anthropicDefaultModelId } From 2db2af01f2bc2e501ddd311b539c395365d26624 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:53:51 +0300 Subject: [PATCH 67/89] refactor(api): use canonical model cache provider identifiers (#1020) * refactor(api): use canonical model cache provider identifiers * test(api): address model cache review feedback * refactor(api): retain auth-scoped provider set --------- Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- .../fetchers/__tests__/modelCache.spec.ts | 154 +++++++++++++----- src/api/providers/fetchers/modelCache.ts | 77 +++++---- src/eslint-suppressions.json | 5 - 3 files changed, 156 insertions(+), 80 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 23586b1364..169e4a0efa 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -58,7 +58,8 @@ vi.mock("../../../core/config/ContextProxy", () => ({ })) // Then imports -import type { Mock } from "vitest" +import type { Mock, Mocked } from "vitest" +import { providerIdentifiers } from "@roo-code/types" import * as fsSync from "fs" import NodeCache from "node-cache" import { getModels, getModelsFromCache } from "../modelCache" @@ -93,7 +94,7 @@ describe("getModels with new GetModelsOptions", () => { mockGetLiteLLMModels.mockResolvedValue(mockModels) const result = await getModels({ - provider: "litellm", + provider: providerIdentifiers.litellm, apiKey: "test-api-key", baseUrl: "http://localhost:4000", }) @@ -113,7 +114,24 @@ describe("getModels with new GetModelsOptions", () => { } mockGetOpenRouterModels.mockResolvedValue(mockModels) - const result = await getModels({ provider: "openrouter" }) + const result = await getModels({ provider: providerIdentifiers.openrouter }) + + expect(mockGetOpenRouterModels).toHaveBeenCalled() + expect(result).toEqual(mockModels) + }) + + it("dispatches OpenRouter through its canonical provider identifier", async () => { + const mockModels = { + "openrouter/canonical-model": { + maxTokens: 8192, + contextWindow: 128000, + supportsPromptCache: false, + }, + } + + mockGetOpenRouterModels.mockResolvedValue(mockModels) + + const result = await getModels({ provider: providerIdentifiers.openrouter }) expect(mockGetOpenRouterModels).toHaveBeenCalled() expect(result).toEqual(mockModels) @@ -130,12 +148,33 @@ describe("getModels with new GetModelsOptions", () => { } mockGetRequestyModels.mockResolvedValue(mockModels) - const result = await getModels({ provider: "requesty", apiKey: DUMMY_REQUESTY_KEY }) + const result = await getModels({ provider: providerIdentifiers.requesty, apiKey: DUMMY_REQUESTY_KEY }) expect(mockGetRequestyModels).toHaveBeenCalledWith(undefined, DUMMY_REQUESTY_KEY) expect(result).toEqual(mockModels) }) + it("dispatches credentialed fetchers through canonical provider identifiers", async () => { + const mockModels = { + "requesty/canonical-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + }, + } + + mockGetRequestyModels.mockResolvedValue(mockModels) + + const result = await getModels({ + provider: providerIdentifiers.requesty, + apiKey: DUMMY_REQUESTY_KEY, + baseUrl: "https://router.requesty.ai/v1", + }) + + expect(mockGetRequestyModels).toHaveBeenCalledWith("https://router.requesty.ai/v1", DUMMY_REQUESTY_KEY) + expect(result).toEqual(mockModels) + }) + it("calls getKenariModels with optional API key", async () => { const mockModels = { "glm-5-2": { @@ -147,7 +186,7 @@ describe("getModels with new GetModelsOptions", () => { } mockGetKenariModels.mockResolvedValue(mockModels) - const result = await getModels({ provider: "kenari", apiKey: "kenari-key-for-testing" }) + const result = await getModels({ provider: providerIdentifiers.kenari, apiKey: "kenari-key-for-testing" }) expect(mockGetKenariModels).toHaveBeenCalledWith("kenari-key-for-testing") expect(result).toEqual(mockModels) @@ -159,7 +198,7 @@ describe("getModels with new GetModelsOptions", () => { await expect( getModels({ - provider: "litellm", + provider: providerIdentifiers.litellm, apiKey: "test-api-key", baseUrl: "http://localhost:4000", }), @@ -178,7 +217,7 @@ describe("getModels with new GetModelsOptions", () => { mockGetMoonshotModels.mockResolvedValue(mockModels) const result = await getModels({ - provider: "moonshot", + provider: providerIdentifiers.moonshot, apiKey: "test-key", baseUrl: "https://api.moonshot.ai/v1", }) @@ -190,7 +229,7 @@ describe("getModels with new GetModelsOptions", () => { it("validates exhaustive provider checking with unknown provider", async () => { // This test ensures TypeScript catches unknown providers at compile time // In practice, the discriminated union should prevent this at compile time - const unknownProvider = "unknown" as any + const unknownProvider = "unknown" as typeof providerIdentifiers.openrouter await expect( getModels({ @@ -201,13 +240,13 @@ describe("getModels with new GetModelsOptions", () => { }) describe("getModelsFromCache disk fallback", () => { - let mockCache: any + let mockCache: Mocked beforeEach(() => { vi.clearAllMocks() // Get the mock cache instance const MockedNodeCache = vi.mocked(NodeCache) - mockCache = new MockedNodeCache() + mockCache = vi.mocked(new MockedNodeCache()) // Reset memory cache to always miss mockCache.get.mockReturnValue(undefined) // Reset fs mocks @@ -218,7 +257,7 @@ describe("getModelsFromCache disk fallback", () => { it("returns undefined when both memory and disk cache miss", () => { vi.mocked(fsSync.existsSync).mockReturnValue(false) - const result = getModelsFromCache("openrouter") + const result = getModelsFromCache(providerIdentifiers.openrouter) expect(result).toBeUndefined() }) @@ -234,13 +273,30 @@ describe("getModelsFromCache disk fallback", () => { mockCache.get.mockReturnValue(memoryModels) - const result = getModelsFromCache("openrouter") + const result = getModelsFromCache(providerIdentifiers.openrouter) expect(result).toEqual(memoryModels) // Disk should not be checked when memory cache hits expect(fsSync.existsSync).not.toHaveBeenCalled() }) + it("isolates authenticated users through the canonical Zoo Gateway identifier", () => { + const previousUserModels = { + "previous-user/model": { + maxTokens: 4096, + contextWindow: 128000, + supportsPromptCache: false, + }, + } + + mockCache.get.mockReturnValue(previousUserModels) + + const result = getModelsFromCache(providerIdentifiers.zooGateway) + + expect(result).toBeUndefined() + expect(mockCache.get).not.toHaveBeenCalled() + }) + it("returns disk cache data when memory cache misses and context is available", () => { // Note: This test validates the logic but the ContextProxy mock in test environment // returns undefined for getCacheDirectoryPathSync, which is expected behavior @@ -257,7 +313,7 @@ describe("getModelsFromCache disk fallback", () => { vi.mocked(fsSync.existsSync).mockReturnValue(true) vi.mocked(fsSync.readFileSync).mockReturnValue(JSON.stringify(diskModels)) - const result = getModelsFromCache("openrouter") + const result = getModelsFromCache(providerIdentifiers.openrouter) // In the test environment, ContextProxy.instance may not be fully initialized, // so getCacheDirectoryPathSync returns undefined and disk cache is not attempted @@ -272,7 +328,7 @@ describe("getModelsFromCache disk fallback", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(function () {}) - const result = getModelsFromCache("openrouter") + const result = getModelsFromCache(providerIdentifiers.openrouter) expect(result).toBeUndefined() expect(consoleErrorSpy).toHaveBeenCalled() @@ -286,7 +342,7 @@ describe("getModelsFromCache disk fallback", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(function () {}) - const result = getModelsFromCache("openrouter") + const result = getModelsFromCache(providerIdentifiers.openrouter) expect(result).toBeUndefined() expect(consoleErrorSpy).toHaveBeenCalled() @@ -296,15 +352,15 @@ describe("getModelsFromCache disk fallback", () => { }) describe("empty cache protection", () => { - let mockCache: any - let mockGet: Mock - let mockSet: Mock + let mockCache: Mocked + let mockGet: Mocked["get"] + let mockSet: Mocked["set"] beforeEach(() => { vi.clearAllMocks() // Get the mock cache instance const MockedNodeCache = vi.mocked(NodeCache) - mockCache = new MockedNodeCache() + mockCache = vi.mocked(new MockedNodeCache()) mockGet = mockCache.get mockSet = mockCache.set // Reset memory cache to always miss by default @@ -316,7 +372,7 @@ describe("empty cache protection", () => { // API returns empty object (simulating failure) mockGetOpenRouterModels.mockResolvedValue({}) - const result = await getModels({ provider: "openrouter" }) + const result = await getModels({ provider: providerIdentifiers.openrouter }) // Should return empty but NOT cache it expect(result).toEqual({}) @@ -334,7 +390,7 @@ describe("empty cache protection", () => { } mockGetOpenRouterModels.mockResolvedValue(mockModels) - const result = await getModels({ provider: "openrouter" }) + const result = await getModels({ provider: providerIdentifiers.openrouter }) expect(result).toEqual(mockModels) expect(mockSet).toHaveBeenCalledWith("openrouter", mockModels) @@ -358,7 +414,7 @@ describe("empty cache protection", () => { mockGetOpenRouterModels.mockResolvedValue({}) const { refreshModels } = await import("../modelCache") - const result = await refreshModels({ provider: "openrouter" }) + const result = await refreshModels({ provider: providerIdentifiers.openrouter }) // Should return existing cache, not empty expect(result).toEqual(existingModels) @@ -388,7 +444,7 @@ describe("empty cache protection", () => { mockGetOpenRouterModels.mockResolvedValue(newModels) const { refreshModels } = await import("../modelCache") - const result = await refreshModels({ provider: "openrouter" }) + const result = await refreshModels({ provider: providerIdentifiers.openrouter }) // Should return new models expect(result).toEqual(newModels) @@ -410,7 +466,7 @@ describe("empty cache protection", () => { mockGetOpenRouterModels.mockRejectedValue(new Error("API error")) const { refreshModels } = await import("../modelCache") - const result = await refreshModels({ provider: "openrouter" }) + const result = await refreshModels({ provider: providerIdentifiers.openrouter }) // Should return existing cache on error expect(result).toEqual(existingModels) @@ -421,7 +477,7 @@ describe("empty cache protection", () => { mockGetOpenRouterModels.mockRejectedValue(new Error("API error")) const { refreshModels } = await import("../modelCache") - const result = await refreshModels({ provider: "openrouter" }) + const result = await refreshModels({ provider: providerIdentifiers.openrouter }) // Should return empty when no cache and API fails expect(result).toEqual({}) @@ -434,7 +490,7 @@ describe("empty cache protection", () => { mockGetOpenRouterModels.mockResolvedValue({}) const { refreshModels } = await import("../modelCache") - const result = await refreshModels({ provider: "openrouter" }) + const result = await refreshModels({ provider: providerIdentifiers.openrouter }) // Should return empty but NOT cache it expect(result).toEqual({}) @@ -462,8 +518,8 @@ describe("empty cache protection", () => { const { refreshModels } = await import("../modelCache") // Start two concurrent refresh calls - const promise1 = refreshModels({ provider: "openrouter" }) - const promise2 = refreshModels({ provider: "openrouter" }) + const promise1 = refreshModels({ provider: providerIdentifiers.openrouter }) + const promise2 = refreshModels({ provider: providerIdentifiers.openrouter }) // API should only be called once (second call reuses in-flight request) expect(mockGetOpenRouterModels).toHaveBeenCalledTimes(1) @@ -495,8 +551,8 @@ describe("empty cache protection", () => { // Different keys -> separate compound keys -> two distinct fetches. const [a, b] = await Promise.all([ - refreshModels({ provider: "requesty", apiKey: "key-one" }), - refreshModels({ provider: "requesty", apiKey: "key-two" }), + refreshModels({ provider: providerIdentifiers.requesty, apiKey: "key-one" }), + refreshModels({ provider: providerIdentifiers.requesty, apiKey: "key-two" }), ]) expect(mockGetRequestyModels).toHaveBeenCalledTimes(2) expect(a).toEqual(mockModels) @@ -512,8 +568,8 @@ describe("empty cache protection", () => { }), ) - const shared1 = refreshModels({ provider: "requesty", apiKey: "same-key" }) - const shared2 = refreshModels({ provider: "requesty", apiKey: "same-key" }) + const shared1 = refreshModels({ provider: providerIdentifiers.requesty, apiKey: "same-key" }) + const shared2 = refreshModels({ provider: providerIdentifiers.requesty, apiKey: "same-key" }) expect(mockGetRequestyModels).toHaveBeenCalledTimes(1) @@ -529,10 +585,10 @@ describe("key-scoped cache key derivation", () => { // Exercises the per-API-key cache discriminator that all KEY_SCOPED_PROVIDERS share. // Requesty is used only because it is a key-scoped provider with a mocked fetcher; the // behavior under test is provider-agnostic. - const keyScopedProvider = "requesty" as const + const keyScopedProvider = providerIdentifiers.requesty - let mockCache: any - let mockSet: Mock + let mockCache: Mocked + let mockSet: Mocked["set"] const mockModels = { "key-scoped/model": { @@ -546,7 +602,7 @@ describe("key-scoped cache key derivation", () => { beforeEach(() => { vi.clearAllMocks() const MockedNodeCache = vi.mocked(NodeCache) - mockCache = new MockedNodeCache() + mockCache = vi.mocked(new MockedNodeCache()) mockCache.get.mockReturnValue(undefined) mockSet = mockCache.set mockGetRequestyModels.mockResolvedValue(mockModels) @@ -627,7 +683,11 @@ describe("compound cache key derivation across scoping dimensions", () => { } it("includes both the server URL and the key discriminator for url+key-scoped providers", async () => { - await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000" }) + await getModels({ + provider: providerIdentifiers.litellm, + apiKey: "compound-key", + baseUrl: "http://host:4000", + }) const cacheKey = writtenCacheKey() // Expected shape: provider:url:keyDiscriminator @@ -635,18 +695,26 @@ describe("compound cache key derivation across scoping dimensions", () => { }) it("normalizes trailing slashes in the server URL so equivalent URLs share a cache key", async () => { - await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000/" }) + await getModels({ + provider: providerIdentifiers.litellm, + apiKey: "compound-key", + baseUrl: "http://host:4000/", + }) const withSlash = writtenCacheKey() mockSet.mockClear() - await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000" }) + await getModels({ + provider: providerIdentifiers.litellm, + apiKey: "compound-key", + baseUrl: "http://host:4000", + }) const withoutSlash = writtenCacheKey() expect(withSlash).toEqual(withoutSlash) }) it("includes only the server URL when a url-scoped provider has no API key", async () => { - await getModels({ provider: "litellm", baseUrl: "http://host:4000" }) + await getModels({ provider: providerIdentifiers.litellm, baseUrl: "http://host:4000" }) const cacheKey = writtenCacheKey() // No trailing key discriminator when apiKey is absent. @@ -654,7 +722,11 @@ describe("compound cache key derivation across scoping dimensions", () => { }) it("falls back to the bare provider name for providers that are neither url- nor key-scoped", async () => { - await getModels({ provider: "openrouter", apiKey: "ignored-key", baseUrl: "http://ignored:4000" }) + await getModels({ + provider: providerIdentifiers.openrouter, + apiKey: "ignored-key", + baseUrl: "http://ignored:4000", + }) const cacheKey = writtenCacheKey() expect(cacheKey).toBe("openrouter") diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index f56ddba34f..af956548ef 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -7,7 +7,7 @@ import NodeCache from "node-cache" import { z } from "zod" import type { ProviderName, ModelRecord } from "@roo-code/types" -import { modelInfoSchema, TelemetryEventName } from "@roo-code/types" +import { modelInfoSchema, providerIdentifiers, TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { safeWriteJson } from "../../../utils/safeWriteJson" @@ -43,33 +43,36 @@ const modelRecordSchema = z.record(z.string(), modelInfoSchema) // deduplicate each other's in-flight refreshes. const inFlightRefresh = new Map>() -// Providers whose model lists are scoped to the signed-in user (e.g. per-account -// allowlists or org policies). For these we MUST NOT cache results on disk or -// in memory: a sign-in/out cycle could otherwise serve a previous user's model -// list to the next user, and stale data could mask backend allowlist updates. -const AUTH_SCOPED_PROVIDERS: ReadonlySet = new Set(["zoo-gateway", "kimi-code"]) - // Providers whose model list is determined by the server URL, not just by the provider name. // Each unique baseUrl must be cached independently so that switching endpoints never serves // stale results from a previously-cached server. const URL_SCOPED_PROVIDERS: ReadonlySet = new Set([ - "litellm", - "poe", - "deepseek", - "moonshot", - "ollama", - "lmstudio", - "requesty", + providerIdentifiers.litellm, + providerIdentifiers.poe, + providerIdentifiers.deepseek, + providerIdentifiers.moonshot, + providerIdentifiers.ollama, + providerIdentifiers.lmstudio, + providerIdentifiers.requesty, ]) // Providers where the API key itself determines which models are visible (e.g. per-key // allowlists). For these the cache key also includes a short hash of // the API key so that two different keys on the same server never share a cache entry. const KEY_SCOPED_PROVIDERS: ReadonlySet = new Set([ - "litellm", // Per-key model allowlists are a first-class LiteLLM proxy feature - "poe", // Per-account model availability - "requesty", // Per-account custom model policies - "moonshot", // Per-key model visibility (api.moonshot.ai vs api.moonshot.cn) + providerIdentifiers.litellm, // Per-key model allowlists are a first-class LiteLLM proxy feature + providerIdentifiers.poe, // Per-account model availability + providerIdentifiers.requesty, // Per-account custom model policies + providerIdentifiers.moonshot, // Per-key model visibility (api.moonshot.ai vs api.moonshot.cn) +]) + +// Providers whose model lists are scoped to the signed-in user (e.g. per-account +// allowlists or org policies). For these we MUST NOT cache results on disk or +// in memory: a sign-in/out cycle could otherwise serve a previous user's model +// list to the next user, and stale data could mask backend allowlist updates. +const AUTH_SCOPED_PROVIDERS: ReadonlySet = new Set([ + providerIdentifiers.zooGateway, + providerIdentifiers.kimiCode, ]) function isAuthScopedProvider(provider: RouterName): boolean { @@ -191,47 +194,47 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { setTimeout(async () => { // Providers that work without API keys const publicProviders: Array<{ provider: RouterName; options: GetModelsOptions }> = [ - { provider: "openrouter", options: { provider: "openrouter" } }, - { provider: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { + provider: providerIdentifiers.openrouter, + options: { provider: providerIdentifiers.openrouter }, + }, + { + provider: providerIdentifiers.vercelAiGateway, + options: { provider: providerIdentifiers.vercelAiGateway }, + }, ] // Refresh each provider in background (fire and forget) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index a0f00c04e3..5774c7768d 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -349,11 +349,6 @@ "count": 3 } }, - "api/providers/fetchers/__tests__/modelCache.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, "api/providers/fetchers/__tests__/modelEndpointCache.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 9 From 7710513aa750927602fe1de03d35c45b595d40f7 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:46:43 +0300 Subject: [PATCH 68/89] refactor(shared): use canonical profile provider identifiers (#1019) * refactor(shared): use canonical profile provider identifiers * test(shared): strengthen profile validator coverage * test(shared): avoid mutating provider identifiers * chore(lint): prune profile validator suppressions --- src/eslint-suppressions.json | 5 - src/shared/ProfileValidator.ts | 42 +++---- src/shared/__tests__/ProfileValidator.spec.ts | 105 ++++++++++++++---- 3 files changed, 107 insertions(+), 45 deletions(-) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 5774c7768d..79506ec786 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1684,11 +1684,6 @@ "count": 2 } }, - "shared/__tests__/ProfileValidator.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, "shared/__tests__/api.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 8 diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index bb56718a7d..923582bd06 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -1,4 +1,4 @@ -import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types" +import { providerIdentifiers, type ProviderSettings, type OrganizationAllowList } from "@roo-code/types" export class ProfileValidator { public static isProfileAllowed(profile: ProviderSettings, allowList: OrganizationAllowList): boolean { @@ -51,36 +51,36 @@ export class ProfileValidator { private static getModelIdFromProfile(profile: ProviderSettings): string | undefined { switch (profile.apiProvider) { - case "openai": + case providerIdentifiers.openai: return profile.openAiModelId - case "anthropic": - case "openai-native": - case "bedrock": - case "vertex": - case "gemini": - case "mistral": - case "deepseek": - case "xai": - case "sambanova": - case "fireworks": - case "friendli": + case providerIdentifiers.anthropic: + case providerIdentifiers.openaiNative: + case providerIdentifiers.bedrock: + case providerIdentifiers.vertex: + case providerIdentifiers.gemini: + case providerIdentifiers.mistral: + case providerIdentifiers.deepseek: + case providerIdentifiers.xai: + case providerIdentifiers.sambanova: + case providerIdentifiers.fireworks: + case providerIdentifiers.friendli: return profile.apiModelId - case "litellm": + case providerIdentifiers.litellm: return profile.litellmModelId - case "lmstudio": + case providerIdentifiers.lmstudio: return profile.lmStudioModelId - case "vscode-lm": + case providerIdentifiers.vscodeLm: // We probably need something more flexible for this one, if we need to really support it here. return profile.vsCodeLmModelSelector?.id - case "openrouter": + case providerIdentifiers.openrouter: return profile.openRouterModelId - case "ollama": + case providerIdentifiers.ollama: return profile.ollamaModelId - case "requesty": + case providerIdentifiers.requesty: return profile.requestyModelId - case "unbound": + case providerIdentifiers.unbound: return profile.unboundModelId - case "fake-ai": + case providerIdentifiers.fakeAi: default: return undefined } diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index efeb7a4820..ace96c9fd0 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -1,11 +1,78 @@ // npx vitest run src/shared/__tests__/ProfileValidator.spec.ts -import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types" +import { providerIdentifiers, type ProviderSettings, type OrganizationAllowList } from "@roo-code/types" import { ProfileValidator } from "../ProfileValidator" describe("ProfileValidator", () => { describe("isProfileAllowed", () => { + it.each([ + ["openai", { openAiModelId: "model" }], + ["anthropic", { apiModelId: "model" }], + ["openaiNative", { apiModelId: "model" }], + ["bedrock", { apiModelId: "model" }], + ["vertex", { apiModelId: "model" }], + ["gemini", { apiModelId: "model" }], + ["mistral", { apiModelId: "model" }], + ["deepseek", { apiModelId: "model" }], + ["xai", { apiModelId: "model" }], + ["sambanova", { apiModelId: "model" }], + ["fireworks", { apiModelId: "model" }], + ["friendli", { apiModelId: "model" }], + ["litellm", { litellmModelId: "model" }], + ["lmstudio", { lmStudioModelId: "model" }], + ["vscodeLm", { vsCodeLmModelSelector: { id: "model" } }], + ["openrouter", { openRouterModelId: "model" }], + ["ollama", { ollamaModelId: "model" }], + ["requesty", { requestyModelId: "model" }], + ["unbound", { unboundModelId: "model" }], + ])("resolves %s model fields through canonical identifiers", (identifierKey, profileSettings) => { + const canonicalIdentifier = providerIdentifiers[identifierKey as keyof typeof providerIdentifiers] + const modelId = "model" + + const profile = { + apiProvider: canonicalIdentifier, + ...profileSettings, + } as ProviderSettings + const allowList: OrganizationAllowList = { + allowAll: false, + providers: { + [canonicalIdentifier]: { allowAll: false, models: [modelId] }, + }, + } + + expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true) + + const negativeAllowList: OrganizationAllowList = { + allowAll: false, + providers: { + [canonicalIdentifier]: { allowAll: false, models: ["other-model"] }, + }, + } + + expect(ProfileValidator.isProfileAllowed(profile, negativeAllowList)).toBe(false) + }) + + it.each([ + { providerAllowAll: false, expected: false }, + { providerAllowAll: true, expected: true }, + ])( + "preserves missing-model fallback behavior when provider allowAll is $providerAllowAll", + ({ providerAllowAll, expected }) => { + const profile: ProviderSettings = { + apiProvider: providerIdentifiers.openai, + } + const allowList: OrganizationAllowList = { + allowAll: false, + providers: { + [providerIdentifiers.openai]: { allowAll: providerAllowAll }, + }, + } + + expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(expected) + }, + ) + it("should allow any profile when allowAll is true", () => { const allowList: OrganizationAllowList = { allowAll: true, @@ -168,17 +235,17 @@ describe("ProfileValidator", () => { // Test specific providers that use apiModelId const apiModelProviders = [ - "anthropic", - "openai-native", - "bedrock", - "vertex", - "gemini", - "mistral", - "deepseek", - "xai", - "sambanova", - "fireworks", - "friendli", + providerIdentifiers.anthropic, + providerIdentifiers.openaiNative, + providerIdentifiers.bedrock, + providerIdentifiers.vertex, + providerIdentifiers.gemini, + providerIdentifiers.mistral, + providerIdentifiers.deepseek, + providerIdentifiers.xai, + providerIdentifiers.sambanova, + providerIdentifiers.fireworks, + providerIdentifiers.friendli, ] apiModelProviders.forEach((provider) => { @@ -190,7 +257,7 @@ describe("ProfileValidator", () => { }, } const profile: ProviderSettings = { - apiProvider: provider as any, // Type assertion needed here + apiProvider: provider, apiModelId: "test-model", } @@ -203,11 +270,11 @@ describe("ProfileValidator", () => { const allowList: OrganizationAllowList = { allowAll: false, providers: { - litellm: { allowAll: false, models: ["test-model"] }, + [providerIdentifiers.litellm]: { allowAll: false, models: ["test-model"] }, }, } const profile: ProviderSettings = { - apiProvider: "litellm" as any, + apiProvider: providerIdentifiers.litellm, litellmModelId: "test-model", } @@ -218,11 +285,11 @@ describe("ProfileValidator", () => { const allowList: OrganizationAllowList = { allowAll: false, providers: { - "vscode-lm": { allowAll: false, models: ["copilot-gpt-3.5"] }, + [providerIdentifiers.vscodeLm]: { allowAll: false, models: ["copilot-gpt-3.5"] }, }, } const profile: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { id: "copilot-gpt-3.5" }, } @@ -278,11 +345,11 @@ describe("ProfileValidator", () => { const allowList: OrganizationAllowList = { allowAll: false, providers: { - "fake-ai": { allowAll: false }, + [providerIdentifiers.fakeAi]: { allowAll: false }, }, } const profile: ProviderSettings = { - apiProvider: "fake-ai", + apiProvider: providerIdentifiers.fakeAi, } expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(false) From 782e28ed0fdde70b2e16820dee05f55ae59c2c0e Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:47:00 +0300 Subject: [PATCH 69/89] refactor(core): use canonical provider identifiers (#1022) * refactor(core): use canonical provider identifiers * test(core): tighten canonical provider coverage * test(core): avoid mutating provider identifiers * test(core): address canonical identifier review feedback * chore(lint): update test suppression counts * test(core): keep existing non-null assertions --- src/core/task/Task.ts | 3 +- src/core/task/__tests__/Task.spec.ts | 265 ++++++++++++------ src/core/webview/ClineProvider.ts | 9 +- .../webview/__tests__/ClineProvider.spec.ts | 35 ++- .../__tests__/webviewMessageHandler.spec.ts | 2 +- src/core/webview/webviewMessageHandler.ts | 5 +- src/eslint-suppressions.json | 4 +- 7 files changed, 226 insertions(+), 97 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7bee3c5e14..da593f0553 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -54,6 +54,7 @@ import { ConsecutiveMistakeError, MAX_MCP_TOOLS_THRESHOLD, countEnabledMcpTools, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -4220,7 +4221,7 @@ export class Task extends EventEmitter implements TaskLike { // but uses allowedFunctionNames to restrict which tools can be called. // Other providers (Anthropic, OpenAI, etc.) don't support this feature yet, // so they continue to receive only the filtered tools for the current mode. - const supportsAllowedFunctionNames = apiConfiguration?.apiProvider === "gemini" + const supportsAllowedFunctionNames = apiConfiguration?.apiProvider === providerIdentifiers.gemini { const provider = this.providerRef.deref() diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 0c784821c1..0b4201d135 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6,7 +6,13 @@ import * as path from "path" import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" -import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types" +import { + providerIdentifiers, + type GlobalState, + type ProviderSettings, + type ModelInfo, + type TaskLike, +} from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../Task" @@ -17,6 +23,27 @@ import { ApiStreamChunk } from "../../../api/transform/stream" import { ContextProxy } from "../../config/ContextProxy" import { processUserContentMentions } from "../../mentions/processUserContentMentions" import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-search-replace" +import type { ApiMessage } from "../../task-persistence" + +type TaskTestAccess = { + getSystemPrompt: () => Promise + startTask: (task?: string, images?: string[]) => Promise + resumeTaskFromHistory: () => Promise + presentAssistantMessageSafe: () => void + updateClineMessage: (message: import("@roo-code/types").ClineMessage) => Promise + saveClineMessages: () => Promise +} + +function getTaskTestAccess(task: Task): TaskTestAccess { + return task as unknown as TaskTestAccess +} + +function requireDefined(value: T | null | undefined): T { + if (value == null) { + throw new Error("Expected test value to be defined") + } + return value +} // Mock delay before any imports that might use it vi.mock("delay", () => ({ @@ -161,7 +188,7 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({ vi.mock("../../ignore/RooIgnoreController") vi.mock("../../condense", async (importOriginal) => { - const actual = (await importOriginal()) as any + const actual = await importOriginal() return { ...actual, summarizeConversation: vi.fn().mockResolvedValue({ @@ -274,11 +301,11 @@ describe("Cline", () => { mockOutputChannel, "sidebar", new ContextProxy(mockExtensionContext), - ) as any + ) // Setup mock API configuration mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", // Add API key to mock config } @@ -409,7 +436,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") const mockStream = { async *[Symbol.asyncIterator]() { @@ -437,7 +464,7 @@ describe("Cline", () => { ts: Date.now(), extraProp: "should be removed", }, - ] as any + ] as Array const iterator = cline.attemptApiRequest(0) await iterator.next() @@ -480,7 +507,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(withImages as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(withImages), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(withImages.api, "getModel").mockReturnValue({ id: "claude-3-sonnet", @@ -503,7 +530,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(withoutImages as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(withoutImages), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(withoutImages.api, "getModel").mockReturnValue({ id: "gpt-3.5-turbo", @@ -537,8 +564,8 @@ describe("Cline", () => { const withImagesSpy = vi.spyOn(withImages.api, "createMessage").mockReturnValue(mockStream) const withoutImagesSpy = vi.spyOn(withoutImages.api, "createMessage").mockReturnValue(mockStream) - withImages.apiConversationHistory = conversationHistory as any - withoutImages.apiConversationHistory = conversationHistory as any + withImages.apiConversationHistory = conversationHistory as ApiMessage[] + withoutImages.apiConversationHistory = conversationHistory as ApiMessage[] const withImagesIterator = withImages.attemptApiRequest(0) await withImagesIterator.next() @@ -582,7 +609,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock delay to track countdown timing const mockDelay = vi.fn().mockResolvedValue(undefined) @@ -676,7 +703,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: clock, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") const mockDelay = vi.fn().mockResolvedValue(undefined) vi.spyOn(await import("delay"), "default").mockImplementation(mockDelay) @@ -750,7 +777,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock delay to track countdown timing const mockDelay = vi.fn().mockResolvedValue(undefined) @@ -919,7 +946,7 @@ describe("Cline", () => { beforeEach(() => { vi.clearAllMocks() mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiKey: "test-key", rateLimitSeconds: 5, } @@ -966,7 +993,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1004,7 +1031,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") // Spy on child.say to verify the emitted message type const saySpy = vi.spyOn(child, "say") @@ -1059,7 +1086,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1099,7 +1126,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child.api, "createMessage").mockReturnValue(mockStream) @@ -1125,7 +1152,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1160,7 +1187,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child1 as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child1), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child1.api, "createMessage").mockReturnValue(mockStream) @@ -1185,7 +1212,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child2 as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child2), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child2.api, "createMessage").mockReturnValue(mockStream) @@ -1215,7 +1242,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1250,7 +1277,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child.api, "createMessage").mockReturnValue(mockStream) @@ -1273,7 +1300,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: clock, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1312,7 +1339,7 @@ describe("Cline", () => { vi.clearAllMocks() mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiKey: "test-key", } @@ -1366,7 +1393,7 @@ describe("Cline", () => { // Test with Anthropic provider const anthropicConfig = { ...mockApiConfig, - apiProvider: "anthropic" as const, + apiProvider: providerIdentifiers.anthropic, apiModelId: "gpt-4", } const anthropicTask = new Task({ @@ -1380,7 +1407,7 @@ describe("Cline", () => { // Test with OpenRouter provider and Claude model const openrouterClaudeConfig = { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "anthropic/claude-3-opus", } const openrouterClaudeTask = new Task({ @@ -1393,7 +1420,7 @@ describe("Cline", () => { // Test with OpenRouter provider and non-Claude model const openrouterGptConfig = { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4", } const openrouterGptTask = new Task({ @@ -1415,7 +1442,7 @@ describe("Cline", () => { for (const modelId of claudeModelFormats) { const config = { - apiProvider: "openai" as const, + apiProvider: providerIdentifiers.openai, openAiModelId: modelId, } const task = new Task({ @@ -1444,7 +1471,7 @@ describe("Cline", () => { // Test with no model ID const noModelConfig = { - apiProvider: "openai" as const, + apiProvider: providerIdentifiers.openai, } const noModelTask = new Task({ provider: mockProvider, @@ -1629,7 +1656,7 @@ describe("Cline", () => { }) // Cast to TaskLike to ensure interface compliance - const taskLike = task as any // TaskLike interface from types package + const taskLike: TaskLike = task // Verify abortTask method exists and is callable expect(typeof taskLike.abortTask).toBe("function") @@ -1784,11 +1811,9 @@ describe("Cline", () => { const cancelSpy = vi.spyOn(task, "cancelCurrentRequest") // Mock other dispose operations - vi.spyOn(task.messageQueueService, "removeListener").mockImplementation( - () => task.messageQueueService as any, - ) + vi.spyOn(task.messageQueueService, "removeListener").mockImplementation(() => task.messageQueueService) vi.spyOn(task.messageQueueService, "dispose").mockImplementation(() => {}) - vi.spyOn(task, "removeAllListeners").mockImplementation(() => task as any) + vi.spyOn(task, "removeAllListeners").mockImplementation(() => task) // Call dispose task.dispose() @@ -1806,7 +1831,7 @@ describe("Cline", () => { }) task.currentRequestAbortController = new AbortController() - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") await task.condenseContext() @@ -1823,7 +1848,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") await task.condenseContext() @@ -1842,7 +1867,7 @@ describe("Cline", () => { }) // Mock required methods for attemptApiRequest to work without hanging - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, @@ -1889,7 +1914,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() @@ -1902,6 +1927,55 @@ describe("Cline", () => { expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) + it("configures tool restrictions for Gemini requests", async () => { + const apiConfiguration = { + ...mockApiConfig, + apiProvider: providerIdentifiers.gemini, + } as ProviderSettings + const task = new Task({ + provider: mockProvider, + apiConfiguration, + task: "test task", + startTask: false, + }) + + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: requireDefined(mockApiConfig.apiModelId), + info: { contextWindow: 200000, maxTokens: 4096 } as ModelInfo, + }) + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + const mockStream = (async function* () { + yield { type: "text", text: "response" } as ApiStreamChunk + })() + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + task.apiConversationHistory = [ + { role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() }, + ] + + await task.attemptApiRequest(0).next() + + const [, , metadata] = requireDefined(createMessageSpy.mock.calls[0]) + const tools = requireDefined(metadata?.tools) + const allowedFunctionNames = requireDefined(metadata?.allowedFunctionNames) + const toolNames = tools.map((tool) => { + if (tool.type !== "function") { + throw new Error(`Unexpected tool type: ${tool.type}`) + } + return tool.function.name + }) + + expect(tools.length).toBeGreaterThan(0) + expect(allowedFunctionNames.length).toBeGreaterThan(0) + expect(allowedFunctionNames.every((name) => toolNames.includes(name))).toBe(true) + }) + it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { const task = new Task({ provider: mockProvider, @@ -1930,7 +2004,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, info: { @@ -1991,7 +2065,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const streamIterator = task.attemptApiRequest(0) await expect(streamIterator.next()).resolves.toMatchObject({ @@ -2014,7 +2088,7 @@ describe("Cline", () => { }) // Mock required methods for attemptApiRequest to work without hanging - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, @@ -2061,7 +2135,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() @@ -2082,7 +2156,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, info: { @@ -2126,7 +2200,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] expect(task.currentRequestAbortController).toBeUndefined() @@ -2147,7 +2221,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, info: { @@ -2191,7 +2265,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() @@ -2210,7 +2284,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ id: mockApiConfig.apiModelId!, info: { @@ -2246,7 +2320,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] // First request const iterator1 = task.attemptApiRequest(0) @@ -2284,7 +2358,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task, "getTokenUsage").mockReturnValue({ totalCost: 0, totalTokensIn: 0, @@ -2325,7 +2399,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] let firstCall = true const retryStream = { @@ -2393,7 +2467,7 @@ describe("Cline", () => { }) // Manually trigger start - const startTaskSpy = vi.spyOn(task as any, "startTask").mockImplementation(async () => {}) + const startTaskSpy = vi.spyOn(getTaskTestAccess(task), "startTask").mockImplementation(async () => {}) task.start() expect(startTaskSpy).toHaveBeenCalledTimes(1) @@ -2406,7 +2480,9 @@ describe("Cline", () => { it("should not call startTask if already started via constructor", () => { // Create a task that starts immediately (startTask defaults to true) // but mock startTask to prevent actual execution - const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockImplementation(async () => {}) + const startTaskSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "startTask") + .mockImplementation(async () => {}) const task = new Task({ provider: mockProvider, @@ -2447,9 +2523,11 @@ describe("Cline", () => { it("logs (instead of crashing) when startTask rejects from the constructor", async () => { const boom = new Error("startTask boom") - const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockImplementation(async () => { - throw boom - }) + const startTaskSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "startTask") + .mockImplementation(async () => { + throw boom + }) new Task({ provider: mockProvider, @@ -2467,9 +2545,11 @@ describe("Cline", () => { it("logs (instead of crashing) when resumeTaskFromHistory rejects from the constructor", async () => { const boom = new Error("resume boom") - const resumeSpy = vi.spyOn(Task.prototype as any, "resumeTaskFromHistory").mockImplementation(async () => { - throw boom - }) + const resumeSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "resumeTaskFromHistory") + .mockImplementation(async () => { + throw boom + }) new Task({ provider: mockProvider, @@ -2526,7 +2606,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "startTask").mockImplementation(async () => { + vi.spyOn(getTaskTestAccess(task), "startTask").mockImplementation(async () => { throw boom }) @@ -2556,7 +2636,7 @@ describe("Cline", () => { consoleErrorSpy.mockClear() task.abort = true - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2579,7 +2659,7 @@ describe("Cline", () => { }) expect(task.abort).toBeFalsy() - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2610,7 +2690,7 @@ describe("Cline", () => { // Simulate the TOCTOU race: abort flips between throw and catch. task.abort = true - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2640,7 +2720,7 @@ describe("Cline", () => { consoleErrorSpy.mockClear() expect(task.abort).toBeFalsy() - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2657,9 +2737,11 @@ describe("Cline", () => { // consumer-attached listener — that path must surface as a log, // not an unhandled rejection. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) const task = new Task({ provider: mockProvider, @@ -2689,10 +2771,12 @@ describe("Cline", () => { // Pins the symmetric .catch arm on the fire-and-forget // updateClineMessage call in ask() when finalizing a partial. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) - const saveSpy = vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(true) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) + const saveSpy = vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(true) const task = new Task({ provider: mockProvider, @@ -2728,9 +2812,11 @@ describe("Cline", () => { // in ask() when a new partial ask arrives while the previous partial // is still pending (AskIgnoredError path). const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) const task = new Task({ provider: mockProvider, @@ -2761,10 +2847,12 @@ describe("Cline", () => { // Pins the .catch arm on the fire-and-forget updateClineMessage call // in handleWebviewAskResponse when marking a tool ask as answered. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) - vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(undefined) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) + vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(false) const task = new Task({ provider: mockProvider, @@ -2827,7 +2915,12 @@ describe("Queued message processing after condense", () => { dispose: vi.fn(), } - const provider = new ClineProvider(ctx, output as any, "sidebar", new ContextProxy(ctx)) as any + const provider = new ClineProvider( + ctx, + output as unknown as vscode.OutputChannel, + "sidebar", + new ContextProxy(ctx), + ) provider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebviewWithoutTaskHistory = vi.fn().mockResolvedValue(undefined) @@ -2836,10 +2929,10 @@ describe("Queued message processing after condense", () => { } const apiConfig: ProviderSettings = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", - } as any + } it("processes queued message after condense completes", async () => { const provider = createProvider() @@ -2851,7 +2944,7 @@ describe("Queued message processing after condense", () => { }) // Make condense fast + deterministic - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("system") const submitSpy = vi.spyOn(task, "submitUserMessage").mockResolvedValue(undefined) // Queue a message during condensing @@ -2886,8 +2979,8 @@ describe("Queued message processing after condense", () => { startTask: false, }) - vi.spyOn(taskA as any, "getSystemPrompt").mockResolvedValue("system") - vi.spyOn(taskB as any, "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(taskA), "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(taskB), "getSystemPrompt").mockResolvedValue("system") const spyA = vi.spyOn(taskA, "submitUserMessage").mockResolvedValue(undefined) const spyB = vi.spyOn(taskB, "submitUserMessage").mockResolvedValue(undefined) @@ -2922,7 +3015,7 @@ describe("pushToolResultToUserContent", () => { beforeEach(() => { mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", } @@ -2965,7 +3058,7 @@ describe("pushToolResultToUserContent", () => { mockOutputChannel, "sidebar", new ContextProxy(mockExtensionContext), - ) as any + ) mockProvider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) mockProvider.postStateToWebview = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 643c4f5a22..458120d9a4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -50,6 +50,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, getModelId, isRetiredProvider, + providerIdentifiers, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" import { TaskRegistry } from "../task/TaskRegistry" @@ -479,7 +480,7 @@ export class ClineProvider async performPreparationTasks(cline: Task) { // LMStudio: We need to force model loading in order to read its context // size; we do it now since we're starting a task with that model selected. - if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") { + if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === providerIdentifiers.lmstudio) { try { if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) { await forceFullModelDetailsLoad( @@ -1007,7 +1008,7 @@ export class ClineProvider // Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback // uses .filter() and updates all of them — the early-return guard must match. const allProfiles = await this.providerSettingsManager.listConfig() - const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === providerIdentifiers.zooGateway) if (zooGatewayProfiles.length === 0) { this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") @@ -1868,14 +1869,14 @@ export class ClineProvider // Check if Zoo Gateway is the currently active profile by apiProvider identity, // not by profile name (profile names are user-renameable). - const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const isZooGatewayActive = currentSettings.apiProvider === providerIdentifiers.zooGateway // Always scan ALL profiles and update every zoo-gateway profile with the new token. // This ensures renamed profiles, duplicate profiles, and inactive profiles all stay // in sync. The model lookup in requestRouterModels uses .find() which returns the // first zoo-gateway profile it finds — if that profile has a stale token, requests fail. const allProfiles = await this.providerSettingsManager.listConfig() - const zooProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + const zooProfiles = allProfiles.filter((p) => p.apiProvider === providerIdentifiers.zooGateway) if (zooProfiles.length === 0) { // No existing zoo-gateway profile — create the canonical default. diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 7e111442a9..93f4f2afaa 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -16,6 +16,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, DEFAULT_WRITE_DELAY_MS, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -29,6 +30,7 @@ import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" +import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../../api/providers/fetchers/lmstudio" // Mock setup must come before imports. vi.mock("../../prompts/sections/custom-instructions") @@ -258,6 +260,11 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({ getModelsFromCache: vi.fn().mockReturnValue(undefined), })) +vi.mock("../../../api/providers/fetchers/lmstudio", () => ({ + hasLoadedFullDetails: vi.fn().mockReturnValue(false), + forceFullModelDetailsLoad: vi.fn().mockResolvedValue(undefined), +})) + vi.mock("../../../services/zoo-code-auth", () => ({ getZooCodeBaseUrl: vi.fn(() => "https://www.zoocode.dev"), getCachedZooCodeToken: vi.fn(), @@ -521,6 +528,32 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) + test("loads full model details when preparing an LM Studio task", async () => { + await provider.performPreparationTasks({ + apiConfiguration: { + apiProvider: providerIdentifiers.lmstudio, + lmStudioBaseUrl: "http://localhost:1234", + lmStudioModelId: "test-model", + }, + } as Task) + + expect(forceFullModelDetailsLoad).toHaveBeenCalledWith("http://localhost:1234", "test-model") + }) + + test("does not reload full model details when the LM Studio model is already loaded", async () => { + vi.mocked(hasLoadedFullDetails).mockReturnValue(true) + + await provider.performPreparationTasks({ + apiConfiguration: { + apiProvider: providerIdentifiers.lmstudio, + lmStudioBaseUrl: "http://localhost:1234", + lmStudioModelId: "test-model", + }, + } as Task) + + expect(forceFullModelDetailsLoad).not.toHaveBeenCalled() + }) + test("resolveWebviewView hydrates the saved terminalProfile into the process-wide Terminal state", async () => { const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) // Seed the persisted setting so the real getState() returns it during hydration. @@ -1458,7 +1491,7 @@ describe("ClineProvider", () => { test("handles case when no current task exists", async () => { // Clear the cline stack - ;(provider as any).taskRegistry = new TaskRegistry() + Object.assign(provider, { taskRegistry: new TaskRegistry() }) // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index d843a3a6d9..64ad6804df 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -1495,7 +1495,7 @@ describe("zooCodeSignOut", () => { vi.clearAllMocks() }) - it("disconnects Zoo Code and clears tokens from all zoo-gateway profiles", async () => { + it("disconnects Zoo Code and clears tokens from all Zoo Gateway profiles", async () => { const { disconnectZooCode } = await import("../../../services/zoo-code-auth") const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) const saveConfig = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a72c4955a3..3126d4ebf5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -22,6 +22,7 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, getCompletionCheckpoint, + providerIdentifiers, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -2773,11 +2774,11 @@ export const webviewMessageHandler = async ( const allProfiles = await provider.providerSettingsManager.listConfig() // Check if Zoo Gateway is the currently active profile by apiProvider identity const currentSettings = provider.contextProxy.getProviderSettings() - const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const isZooGatewayActive = currentSettings.apiProvider === providerIdentifiers.zooGateway const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName for (const entry of allProfiles) { - if (entry.apiProvider !== "zoo-gateway") { + if (entry.apiProvider !== providerIdentifiers.zooGateway) { continue } diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 79506ec786..4b0f48253f 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -876,7 +876,7 @@ }, "core/task/__tests__/Task.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 93 + "count": 31 } }, "core/task/__tests__/Task.sticky-profile-race.spec.ts": { @@ -1106,7 +1106,7 @@ }, "core/webview/__tests__/ClineProvider.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 199 + "count": 198 } }, "core/webview/__tests__/ClineProvider.sticky-mode.spec.ts": { From d27153a251d2051b6a8e73d305b06ffbc5ac6970 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:50:19 -0400 Subject: [PATCH 70/89] fix(ripgrep): support @vscode/ripgrep >=1.18 platform-package layout (#1024) (#1032) * fix(ripgrep): resolve platform-specific binary for @vscode/ripgrep >=1.18 * fix(environment): gracefully handle ripgrep not found during file listing * fix(ripgrep): use npm_config_arch for platform-package resolution * refactor(ripgrep): replace createRequire resolver with static candidate paths * fix(ripgrep): evaluate npm_config_arch at call time in candidate paths --------- Co-authored-by: Naved Merchant --- .../__tests__/getEnvironmentDetails.spec.ts | 18 ++++++++ src/core/environment/getEnvironmentDetails.ts | 28 ++++++----- src/services/ripgrep/__tests__/index.spec.ts | 46 +++++++++++++++++++ src/services/ripgrep/index.ts | 12 +++-- 4 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 1fcdac0ed7..df47e83c21 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -446,4 +446,22 @@ describe("getEnvironmentDetails", () => { expect(getGitStatus).toHaveBeenCalledWith(mockCwd, 5) }) + + // Regression test for https://github.com/Zoo-Code-Org/Zoo-Code/issues/1024 + // When ripgrep cannot be found (e.g. @vscode/ripgrep >=1.18 platform layout on + // Windows), listFiles throws "Could not find ripgrep binary". getEnvironmentDetails + // must not propagate this — the task should proceed to the API call, not hang at 0%. + it("should degrade gracefully when listFiles rejects with an Error", async () => { + ;(listFiles as Mock).mockRejectedValue(new Error("Could not find ripgrep binary")) + + const result = await getEnvironmentDetails(mockCline as Task, true) + expect(result).toContain("File listing unavailable: Could not find ripgrep binary") + }) + + it("should degrade gracefully when listFiles rejects with a non-Error value", async () => { + ;(listFiles as Mock).mockRejectedValue("unexpected string rejection") + + const result = await getEnvironmentDetails(mockCline as Task, true) + expect(result).toContain("File listing unavailable: unexpected string rejection") + }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 141ffdf3e2..0e7d18a57a 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -241,18 +241,22 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo if (maxFiles === 0) { details += "(Workspace files context disabled. Use list_files to explore if needed.)" } else { - const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles) - const { showRooIgnoredFiles = false } = state ?? {} - - const result = formatResponse.formatFilesList( - cline.cwd, - files, - didHitLimit, - cline.rooIgnoreController, - showRooIgnoredFiles, - ) - - details += result + try { + const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles) + const { showRooIgnoredFiles = false } = state ?? {} + + const result = formatResponse.formatFilesList( + cline.cwd, + files, + didHitLimit, + cline.rooIgnoreController, + showRooIgnoredFiles, + ) + + details += result + } catch (error) { + details += `(File listing unavailable: ${error instanceof Error ? error.message : String(error)})` + } } } } diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index e6a613ad67..defc6d23b2 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -95,4 +95,50 @@ describe("getBinPath", () => { expect(await getBinPath(appRoot)).toBeUndefined() }) + + // Regression test for https://github.com/Zoo-Code-Org/Zoo-Code/issues/1024 + // VS Code 1.130+ ships @vscode/ripgrep >=1.18, where the binary lives in a + // platform-specific optional package (e.g. @vscode/ripgrep-win32-x64). + // None of the previous candidate paths matched this layout. + it("resolves ripgrep from the @vscode/ripgrep >=1.18 platform-package layout", async () => { + const arch = process.env.npm_config_arch || process.arch + const rg = path.join(appRoot, `node_modules/@vscode/ripgrep-${process.platform}-${arch}/bin`, binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + + it("resolves ripgrep from the unpacked @vscode/ripgrep >=1.18 platform-package layout", async () => { + const arch = process.env.npm_config_arch || process.arch + const rg = path.join( + appRoot, + `node_modules.asar.unpacked/@vscode/ripgrep-${process.platform}-${arch}/bin`, + binName, + ) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + + it("respects npm_config_arch when selecting the platform package", async () => { + const overrideArch = "x64" + const original = process.env.npm_config_arch + process.env.npm_config_arch = overrideArch + try { + const rg = path.join( + appRoot, + `node_modules/@vscode/ripgrep-${process.platform}-${overrideArch}/bin`, + binName, + ) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + } finally { + if (original === undefined) { + delete process.env.npm_config_arch + } else { + process.env.npm_config_arch = original + } + } + }) }) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 96f5c34e4b..0d4a25056b 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -90,6 +90,9 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): * resolution) and the diagnostic command (existence report for all paths). */ export function ripgrepCandidatePaths(vscodeAppRoot: string): readonly string[] { + // Read at call time so process.env.npm_config_arch overrides take effect, + // matching @vscode/ripgrep's own arch selection logic. + const platformPkg = `@vscode/ripgrep-${process.platform}-${process.env.npm_config_arch || process.arch}` return [ path.join(vscodeAppRoot, "node_modules/@vscode/ripgrep/bin/", binName), path.join(vscodeAppRoot, "node_modules/vscode-ripgrep/bin", binName), @@ -101,15 +104,18 @@ export function ripgrepCandidatePaths(vscodeAppRoot: string): readonly string[] `node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`, binName, ), + // @vscode/ripgrep >=1.18 (VS Code 1.130+): binary lives in a platform-specific optional package. + path.join(vscodeAppRoot, `node_modules/${platformPkg}/bin`, binName), + path.join(vscodeAppRoot, `node_modules.asar.unpacked/${platformPkg}/bin`, binName), ] } /** * Get the path to the ripgrep binary shipped inside the VS Code installation. * - * Both the long-standing `@vscode/ripgrep` layout and the newer - * `@vscode/ripgrep-universal` layout are checked — the latter is what VS Code - * Insiders' staged-install builds use (see microsoft/vscode#252063). + * Probes all known layouts: classic @vscode/ripgrep, @vscode/ripgrep-universal + * (VS Code Insiders staged-install), and the @vscode/ripgrep >=1.18 + * platform-package layout used by VS Code 1.130+. * * Returns `undefined` when ripgrep cannot be located. */ From 488732ed4dc4506f14b1519f46bf18f1b1a2bf22 Mon Sep 17 00:00:00 2001 From: Lee-Si-Yoon Date: Wed, 29 Jul 2026 08:57:47 +0900 Subject: [PATCH 71/89] feat(friendli): add reasoning params support (#886) --- packages/types/src/providers/friendli.ts | 4 + src/api/providers/__tests__/friendli.spec.ts | 225 ++++++++++++++++++- src/api/providers/friendli.ts | 186 +++++++++++++++ 3 files changed, 414 insertions(+), 1 deletion(-) diff --git a/packages/types/src/providers/friendli.ts b/packages/types/src/providers/friendli.ts index 71591d8f40..b240e5ca34 100644 --- a/packages/types/src/providers/friendli.ts +++ b/packages/types/src/providers/friendli.ts @@ -20,6 +20,8 @@ export const friendliModels = { outputPrice: 4.4, cacheWritesPrice: 0, cacheReadsPrice: 0.26, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "high", description: "GLM-5.2 is Zhipu's flagship model with a 1M context window and 128k max output, served via Friendli Model APIs. It delivers top-tier long-context reasoning, coding, and agentic performance for extended engineering sessions.", }, @@ -33,6 +35,8 @@ export const friendliModels = { outputPrice: 4.4, cacheWritesPrice: 0, cacheReadsPrice: 0.26, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "high", description: "GLM-5.1 is Zhipu's most capable model with a 200k context window and 128k max output, served via Friendli Model APIs. It delivers top-tier reasoning, coding, and agentic performance.", }, diff --git a/src/api/providers/__tests__/friendli.spec.ts b/src/api/providers/__tests__/friendli.spec.ts index 41892d3bc6..c8fd81ad19 100644 --- a/src/api/providers/__tests__/friendli.spec.ts +++ b/src/api/providers/__tests__/friendli.spec.ts @@ -142,7 +142,16 @@ describe("FriendliHandler", () => { }, ])( "should expose newly added model $modelId", - ({ modelId, contextWindow, maxTokens, supportsMaxTokens, inputPrice, outputPrice, cacheWritesPrice, cacheReadsPrice }) => { + ({ + modelId, + contextWindow, + maxTokens, + supportsMaxTokens, + inputPrice, + outputPrice, + cacheWritesPrice, + cacheReadsPrice, + }) => { expect(friendliModels[modelId]).toBeDefined() const info = friendliModels[modelId] as import("@roo-code/types").ModelInfo expect(info.maxTokens).toBe(maxTokens) @@ -394,3 +403,217 @@ describe("Friendli model max output tokens (clamping behavior)", () => { expect(result).toBe(80_000) }) }) + +describe("FriendliHandler — Friendli-specific reasoning params", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should include reasoning_effort, chat_template_kwargs, parse_reasoning for GLM-5.2 with reasoning enabled", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-key", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "zai-org/GLM-5.2", + reasoning_effort: "high", + chat_template_kwargs: { enable_thinking: true }, + parse_reasoning: true, + include_reasoning: true, + }), + undefined, + ) + }) + + it("should send enable_thinking: false when enableReasoningEffort is false on controllable model", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-...ey", + enableReasoningEffort: false, + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBeUndefined() + expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false }) + expect(callArgs.parse_reasoning).toBeUndefined() + expect(callArgs.include_reasoning).toBeUndefined() + }) + + it("should send enable_thinking: false when reasoningEffort is none on controllable model", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-...ey", + enableReasoningEffort: true, + reasoningEffort: "none", + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBeUndefined() + expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false }) + expect(callArgs.parse_reasoning).toBeUndefined() + expect(callArgs.include_reasoning).toBeUndefined() + }) + + it("should send enable_thinking: false when reasoningEffort is disable on controllable model", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-...ey", + enableReasoningEffort: true, + reasoningEffort: "disable", + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBeUndefined() + expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: false }) + expect(callArgs.parse_reasoning).toBeUndefined() + expect(callArgs.include_reasoning).toBeUndefined() + }) + + it("should use model default reasoningEffort when no explicit settings are provided", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-...ey", + // No enableReasoningEffort or reasoningEffort — model default "high" kicks in + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBe("high") + expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true }) + expect(callArgs.parse_reasoning).toBe(true) + expect(callArgs.include_reasoning).toBe(true) + }) + + it("should not include any reasoning params for non-reasoning DeepSeek-V3.2", async () => { + const handler = new FriendliHandler({ + apiModelId: "deepseek-ai/DeepSeek-V3.2", + friendliApiKey: "test-key", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + await handler.createMessage("system", []).next() + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBeUndefined() + expect(callArgs.chat_template_kwargs).toBeUndefined() + expect(callArgs.parse_reasoning).toBeUndefined() + }) + + it("should handle delta.reasoning_content from parse_reasoning=true stream", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-key", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { reasoning_content: "Let me think..." } }], + usage: null, + } + yield { + choices: [{ delta: { content: "The answer is 42" } }], + usage: null, + } + yield { + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, + } + }, + })) + + const stream = handler.createMessage("system", []) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "reasoning", text: "Let me think..." }) + expect(chunks).toContainEqual({ type: "text", text: "The answer is 42" }) + }) + + it("completePrompt should include reasoning params when enabled", async () => { + const handler = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-key", + enableReasoningEffort: true, + reasoningEffort: "medium", + }) + + mockCreate.mockResolvedValueOnce({ + choices: [{ message: { content: "test result" } }], + }) + + await handler.completePrompt("test") + + const callArgs = mockCreate.mock.calls[0][0] as Record + expect(callArgs.reasoning_effort).toBe("medium") + expect(callArgs.chat_template_kwargs).toEqual({ enable_thinking: true }) + expect(callArgs.parse_reasoning).toBe(true) + expect(callArgs.include_reasoning).toBe(true) + }) +}) diff --git a/src/api/providers/friendli.ts b/src/api/providers/friendli.ts index f9aa4fa20c..a5507e355a 100644 --- a/src/api/providers/friendli.ts +++ b/src/api/providers/friendli.ts @@ -1,12 +1,60 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + import { type FriendliModelId, friendliDefaultModelId, friendliModels } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" +import { shouldUseReasoningEffort, getModelMaxOutputTokens } from "../../shared/api" + +import { convertToOpenAiMessages } from "../transform/openai-format" +import { getModelParams } from "../transform/model-params" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" +import { handleOpenAIError } from "./utils/error-handler" +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" + +/** + * Friendli extends the OpenAI Chat Completions API with these non-standard fields: + * - reasoning_effort: enum (minimal, low, medium, high, xhigh, max) — reasoning depth + * - chat_template_kwargs: { enable_thinking: boolean } — toggles thinking for controllable models + * - parse_reasoning / include_reasoning: when true, Friendli streams reasoning via + * delta.reasoning_content (which extractReasoningFromDelta already handles) + * - reasoning_budget: integer token budget (not currently surfaced in settings UI) + * + * The reasoning fields are shared across streaming and non-streaming requests; the base + * `ChatCompletionCreateParams` (non-streaming) variant is used for `completePrompt` while the + * `ChatCompletionCreateParamsStreaming` variant is used for `createStream`. + */ +type FriendliReasoningParams = { + chat_template_kwargs?: { enable_thinking: boolean } + parse_reasoning?: boolean + include_reasoning?: boolean + // Friendli's reasoning_effort supports a broader enum than OpenAI's type allows + reasoning_effort?: + | OpenAI.Chat.Completions.ChatCompletionCreateParams["reasoning_effort"] + | "minimal" + | "xhigh" + | "max" +} + +type FriendliChatCompletionParams = Omit< + OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, + "reasoning_effort" +> & + FriendliReasoningParams + +type FriendliChatCompletionNonStreamingParams = Omit< + OpenAI.Chat.Completions.ChatCompletionCreateParams, + "reasoning_effort" +> & + FriendliReasoningParams /** * Handler for the Friendli Model APIs (OpenAI-compatible). * Routes chat completions to `https://api.friendli.ai/serverless/v1`. + * + * Overrides `createStream` and `completePrompt` to inject Friendli-specific + * reasoning parameters that the base class doesn't know about. */ export class FriendliHandler extends BaseOpenAiCompatibleProvider { /** @@ -23,4 +71,142 @@ export class FriendliHandler extends BaseOpenAiCompatibleProvider { + const { info: modelInfo, reasoningEffort } = this.getModel() + const extra: Partial = {} + + const isControllableReasoning = Array.isArray(modelInfo.supportsReasoningEffort) + + const useReasoningEffort = modelInfo.supportsReasoningEffort + ? shouldUseReasoningEffort({ model: modelInfo, settings: this.options }) + : false + + // User disabled reasoning on a controllable model — explicitly turn thinking off. + // The model's Jinja chat template defaults enable_thinking to true, so omitting + // the param would leave reasoning active (burning tokens against user intent). + if (isControllableReasoning && !useReasoningEffort) { + extra.chat_template_kwargs = { enable_thinking: false } + return extra + } + + // Non-reasoning model — nothing to send + if (!useReasoningEffort) { + return extra + } + + // Reasoning is enabled + extra.parse_reasoning = true + extra.include_reasoning = true + extra.chat_template_kwargs = { enable_thinking: true } + + if (reasoningEffort) { + extra.reasoning_effort = reasoningEffort as FriendliReasoningParams["reasoning_effort"] + } + + return extra + } + + /** + * Override createStream to inject Friendli-specific reasoning params. + * The base class createMessage() calls createStream and handles all stream + * processing (TagMatcher, extractReasoningFromDelta, tool calls, usage). + */ + protected override createStream( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + requestOptions?: OpenAI.RequestOptions, + ) { + const friendliExtra = this.buildFriendliReasoningParams() + + const { id: model, info } = this.getModel() + + // Centralized cap: clamp to 20% of the context window + const max_tokens = + getModelMaxOutputTokens({ + modelId: model, + model: info, + settings: this.options, + format: "openai", + }) ?? undefined + + const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature + + const params: FriendliChatCompletionParams = { + model, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? true, + ...friendliExtra, + } + + try { + return this.client.chat.completions.create( + params as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, + requestOptions, + ) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } + + override async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { + const { id: modelId } = this.getModel() + const friendliExtra = this.buildFriendliReasoningParams() + + const params: FriendliChatCompletionNonStreamingParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + ...friendliExtra, + } + + try { + const requestOptions: OpenAI.RequestOptions | undefined = + options && (options.abortSignal !== undefined || options.timeoutMs !== undefined) + ? { signal: options.abortSignal, timeout: options.timeoutMs } + : undefined + const response = (await this.client.chat.completions.create( + params as OpenAI.Chat.Completions.ChatCompletionCreateParams, + requestOptions, + )) as OpenAI.Chat.Completions.ChatCompletion + return response.choices?.[0]?.message.content || "" + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } } From 85f6f27cb2b21e59002d43c52d1e3f49b13f4cb9 Mon Sep 17 00:00:00 2001 From: Morgan Carr <256248948+morgan-coded@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:44:56 -0500 Subject: [PATCH 72/89] chore(lint): add core/webview to no-floating-promises ratchet (#950) * chore(lint): add core/webview to no-floating-promises ratchet * fix(webview): catch indexing errors and cover changed handler paths * chore(lint): triage new core/webview call sites after merging main Also restore README.md to the upstream version, dropping an unintended formatter artifact picked up during the merge commit. --------- Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- src/core/webview/ClineProvider.ts | 62 +-- .../webview/__tests__/ClineProvider.spec.ts | 460 +++++++++++++++++- src/core/webview/checkpointRestoreHandler.ts | 2 +- src/core/webview/rulesMessageHandler.ts | 6 +- src/core/webview/skillsMessageHandler.ts | 4 +- src/core/webview/webviewMessageHandler.ts | 144 +++--- src/eslint.config.mjs | 2 +- 7 files changed, 568 insertions(+), 112 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 458120d9a4..7bdf03f0a2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -144,7 +144,7 @@ function runDelegationTransition( locks.set(parentTaskId, tail) - tail.finally(() => { + void tail.finally(() => { if (locks.get(parentTaskId) === tail) { locks.delete(parentTaskId) } @@ -228,7 +228,7 @@ export class ClineProvider ClineProvider.activeInstances.add(this) this.mdmService = mdmService - this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) + void this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) // Initialize the per-task file-based history store. // The globalState write-through is debounced separately (not on every mutation) @@ -725,7 +725,7 @@ export class ClineProvider this.mcpHub = undefined await this.skillsManager?.dispose() this.skillsManager = undefined - this.marketplaceManager?.cleanup() + await this.marketplaceManager?.cleanup() this.customModesManager?.dispose() this.taskHistoryStore.dispose() this.flushGlobalStateWriteThrough() @@ -860,9 +860,27 @@ export class ClineProvider setPanel(webviewView, "sidebar") } + // Set up webview options with proper resource roots + const resourceRoots = [this.contextProxy.extensionUri] + + // Add workspace folders to allow access to workspace files + if (vscode.workspace.workspaceFolders) { + resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri)) + } + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: resourceRoots, + } + + webviewView.webview.html = + this.contextProxy.extensionMode === vscode.ExtensionMode.Development + ? await this.getHMRHtmlContent(webviewView.webview) + : await this.getHtmlContent(webviewView.webview) + // Initialize out-of-scope variables that need to receive persistent // global state values. - this.getState().then( + await this.getState().then( ({ terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled = false, @@ -890,24 +908,6 @@ export class ClineProvider }, ) - // Set up webview options with proper resource roots - const resourceRoots = [this.contextProxy.extensionUri] - - // Add workspace folders to allow access to workspace files - if (vscode.workspace.workspaceFolders) { - resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri)) - } - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: resourceRoots, - } - - webviewView.webview.html = - this.contextProxy.extensionMode === vscode.ExtensionMode.Development - ? await this.getHMRHtmlContent(webviewView.webview) - : await this.getHtmlContent(webviewView.webview) - // Sets up an event listener to listen for messages passed from the webview view context // and executes code based on the message that is received. this.setWebviewMessageListener(webviewView.webview) @@ -930,7 +930,7 @@ export class ClineProvider // for this visibility listener panel. const viewStateDisposable = webviewView.onDidChangeViewState(() => { if (this.view?.visible) { - this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) + void this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) } else { this.logWebviewHiddenDiagnostics() } @@ -941,7 +941,7 @@ export class ClineProvider // sidebar const visibilityDisposable = webviewView.onDidChangeVisibility(() => { if (this.view?.visible) { - this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) + void this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) } else { this.logWebviewHiddenDiagnostics() } @@ -2139,7 +2139,7 @@ export class ClineProvider const state = await this.getStateToPostToWebview() this.clineMessagesSeq++ state.clineMessagesSeq = this.clineMessagesSeq - this.postMessageToWebview({ type: "state", state }) + await this.postMessageToWebview({ type: "state", state }) } /** @@ -2155,7 +2155,7 @@ export class ClineProvider this.clineMessagesSeq++ state.clineMessagesSeq = this.clineMessagesSeq const { taskHistory: _omit, ...rest } = state - this.postMessageToWebview({ type: "state", state: rest }) + await this.postMessageToWebview({ type: "state", state: rest }) } /** @@ -2172,7 +2172,7 @@ export class ClineProvider async postStateToWebviewWithoutClineMessages(): Promise { const state = await this.getStateToPostToWebview() const { clineMessages: _omitMessages, taskHistory: _omitHistory, ...rest } = state - this.postMessageToWebview({ type: "state", state: rest }) + await this.postMessageToWebview({ type: "state", state: rest }) } /** @@ -2192,7 +2192,7 @@ export class ClineProvider ]) // Send marketplace data separately - this.postMessageToWebview({ + await this.postMessageToWebview({ type: "marketplaceData", organizationMcps: marketplaceResult.organizationMcps || [], marketplaceItems: marketplaceResult.marketplaceItems || [], @@ -2203,7 +2203,7 @@ export class ClineProvider console.error("Failed to fetch marketplace data:", error) // Send empty data on error to prevent UI from hanging - this.postMessageToWebview({ + await this.postMessageToWebview({ type: "marketplaceData", organizationMcps: [], marketplaceItems: [], @@ -3002,7 +3002,7 @@ export class ClineProvider if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) { // Get the full status from the manager to ensure we have all fields correctly formatted const fullStatus = currentManager.getCurrentStatus() - this.postMessageToWebview({ + void this.postMessageToWebview({ type: "indexingStatusUpdate", values: fullStatus, }) @@ -3014,7 +3014,7 @@ export class ClineProvider } // Send initial status for the current workspace - this.postMessageToWebview({ + void this.postMessageToWebview({ type: "indexingStatusUpdate", values: currentManager.getCurrentStatus(), }) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 93f4f2afaa..1e2c0402bf 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -12,6 +12,7 @@ import { type ClineMessage, type ExtensionMessage, type ExtensionState, + type WebviewMessage, ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, @@ -28,6 +29,7 @@ import { Task, TaskOptions } from "../../task/Task" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" +import { webviewMessageHandler } from "../webviewMessageHandler" import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../../api/providers/fetchers/lmstudio" @@ -169,6 +171,8 @@ vi.mock("vscode", () => ({ showInformationMessage: vi.fn(), showWarningMessage: vi.fn(), showErrorMessage: vi.fn(), + showSaveDialog: vi.fn(), + showOpenDialog: vi.fn(), activeTextEditor: undefined, onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), }, @@ -202,8 +206,39 @@ vi.mock("vscode", () => ({ })) vi.mock("../../../utils/tts", () => ({ + playTts: vi.fn().mockResolvedValue(undefined), setTtsEnabled: vi.fn(), setTtsSpeed: vi.fn(), + stopTts: vi.fn(), +})) + +vi.mock("../../../integrations/misc/open-file", () => ({ + openFile: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../integrations/misc/image-handler", () => ({ + openImage: vi.fn().mockResolvedValue(undefined), + saveImage: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../mentions", () => ({ + openMention: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../utils/export", () => ({ + resolveDefaultSaveUri: vi.fn().mockResolvedValue({ fsPath: "/test/default-export.yaml" }), + saveLastExportPath: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../integrations/openai-codex/oauth", () => ({ + openAiCodexOAuthManager: { + getAccessToken: vi.fn(), + getAccountId: vi.fn(), + }, +})) + +vi.mock("../../../integrations/openai-codex/rate-limits", () => ({ + fetchOpenAiCodexRateLimitInfo: vi.fn(), })) vi.mock("../../../api", () => ({ @@ -219,7 +254,7 @@ vi.mock("../../../integrations/workspace/WorkspaceTracker", () => { return { default: vi.fn().mockImplementation(function () { return { - initializeFilePaths: vi.fn(), + initializeFilePaths: vi.fn().mockResolvedValue(undefined), dispose: vi.fn(), } }), @@ -359,6 +394,8 @@ vi.mock("@roo-code/cloud", () => ({ get instance() { return { isAuthenticated: vi.fn().mockReturnValue(false), + login: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), off: vi.fn(), } }, @@ -785,7 +822,8 @@ describe("ClineProvider", () => { await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as ReturnType).mock + .calls[0][0] // Simulate webviewDidLaunch message await messageHandler({ type: "webviewDidLaunch" }) @@ -794,6 +832,29 @@ describe("ClineProvider", () => { expect(mockPostMessage).toHaveBeenCalled() }) + test("logs detached workspace initialization failures", async () => { + await provider.resolveWebviewView(mockWebviewView) + + let rejectInitialization!: (error: Error) => void + const initializationPromise = new Promise((_, reject) => { + rejectInitialization = reject + }) + const initializeSpy = vi + .spyOn(provider.workspaceTracker!, "initializeFilePaths") + .mockReturnValue(initializationPromise) + const logSpy = vi.spyOn(provider, "log") + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + + await expect(messageHandler({ type: "webviewDidLaunch" })).resolves.toBeUndefined() + expect(initializeSpy).toHaveBeenCalledOnce() + + rejectInitialization(new Error("workspace boom")) + await Promise.resolve() + await Promise.resolve() + + expect(logSpy).toHaveBeenCalledWith("Workspace initialization error: Error: workspace boom") + }) + test("clearTask aborts current task", async () => { // Setup Cline instance with auto-mock from the top of the file const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance @@ -1183,7 +1244,7 @@ describe("ClineProvider", () => { setModeConfig: vi.fn(), } as any - provider.setValue("currentApiConfigName", "current-config") + await provider.setValue("currentApiConfigName", "current-config") // Switch to architect mode await messageHandler({ type: "mode", text: "architect" }) @@ -1281,7 +1342,7 @@ describe("ClineProvider", () => { }, } - provider.setValue("customModePrompts", existingPrompts) + await provider.setValue("customModePrompts", existingPrompts) // Test updating a prompt await messageHandler({ @@ -2273,6 +2334,397 @@ describe("ClineProvider", () => { }) }) +describe("webviewMessageHandler no-floating-promises coverage", () => { + const createProvider = (overrides: Record = {}) => + Object.assign( + { + context: { + secrets: { + get: vi.fn().mockResolvedValue(undefined), + }, + }, + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn().mockResolvedValue(undefined), + }, + postMessageToWebview: vi.fn().mockResolvedValue(true), + postStateToWebview: vi.fn().mockResolvedValue(undefined), + getCurrentTask: vi.fn(), + getCurrentWorkspaceCodeIndexManager: vi.fn(), + getMcpHub: vi.fn().mockReturnValue({ + getMcpSettingsFilePath: vi.fn().mockResolvedValue("/test/mcp.json"), + }), + providerSettingsManager: { + listConfig: vi.fn().mockResolvedValue([]), + }, + customModesManager: { + getCustomModesFilePath: vi.fn().mockResolvedValue("/test/custom-modes.yaml"), + exportModeWithRules: vi.fn(), + importModeWithRules: vi.fn(), + getCustomModes: vi.fn().mockResolvedValue([]), + checkRulesDirectoryHasContent: vi.fn().mockResolvedValue(true), + }, + exportTaskWithId: vi.fn().mockResolvedValue(undefined), + showTaskWithId: vi.fn().mockResolvedValue(undefined), + condenseTaskContext: vi.fn().mockResolvedValue(undefined), + deleteTaskWithId: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + cwd: "/test/workspace", + }, + overrides, + ) as unknown as ClineProvider + + const createIndexManager = (overrides: Record = {}) => + Object.assign( + { + setWorkspaceEnabled: vi.fn().mockResolvedValue(undefined), + setAutoEnableDefault: vi.fn().mockResolvedValue(undefined), + isFeatureEnabled: true, + isFeatureConfigured: true, + isWorkspaceEnabled: true, + initialize: vi.fn().mockResolvedValue(undefined), + state: "Standby", + isInitialized: true, + startIndexing: vi.fn().mockResolvedValue(undefined), + stopIndexing: vi.fn(), + clearIndexData: vi.fn().mockResolvedValue(undefined), + getCurrentStatus: vi.fn().mockReturnValue({ systemStatus: "Standby" }), + }, + overrides, + ) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("logs a detached indexing rejection without rejecting the handler", async () => { + let rejectIndexing!: (error: Error) => void + const indexingPromise = new Promise((_, reject) => { + rejectIndexing = reject + }) + const manager = createIndexManager({ + startIndexing: vi.fn().mockReturnValue(indexingPromise), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await expect(webviewMessageHandler(provider, { type: "startIndexing" })).resolves.toBeUndefined() + expect(manager.startIndexing).toHaveBeenCalledOnce() + + rejectIndexing(new Error("boom")) + await Promise.resolve() + await Promise.resolve() + + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: boom") + }) + + it("covers the changed task-operation happy paths", async () => { + const task = { + taskId: "task-1", + handleTerminalOperation: vi.fn().mockResolvedValue(undefined), + } + const provider = createProvider({ getCurrentTask: vi.fn().mockReturnValue(task) }) + + await webviewMessageHandler(provider, { type: "terminalOperation", terminalOperation: "continue" }) + await webviewMessageHandler(provider, { type: "exportCurrentTask" }) + await webviewMessageHandler(provider, { type: "showTaskWithId", text: "task-2" }) + await webviewMessageHandler(provider, { type: "condenseTaskContextRequest", text: "task-2" }) + await webviewMessageHandler(provider, { type: "deleteTaskWithId", text: "task-2" }) + await webviewMessageHandler(provider, { type: "exportTaskWithId", text: "task-2" }) + + expect(task.handleTerminalOperation).toHaveBeenCalledWith("continue") + expect(provider.exportTaskWithId).toHaveBeenCalledTimes(2) + expect(provider.showTaskWithId).toHaveBeenCalledWith("task-2") + expect(provider.condenseTaskContext).toHaveBeenCalledWith("task-2") + expect(provider.deleteTaskWithId).toHaveBeenCalledWith("task-2") + }) + + it("covers changed file, image, mention, settings, and TTS dispatch paths", async () => { + const { openFile } = await import("../../../integrations/misc/open-file") + const { openImage, saveImage } = await import("../../../integrations/misc/image-handler") + const { openMention } = await import("../../mentions") + const { playTts } = await import("../../../utils/tts") + const provider = createProvider() + + await webviewMessageHandler(provider, { type: "openImage", text: "/test/image.png" }) + await webviewMessageHandler(provider, { type: "saveImage", dataUri: "invalid" }) + await webviewMessageHandler(provider, { type: "openFile", text: "/test/file.ts" }) + await webviewMessageHandler(provider, { type: "openMention", text: "file.ts" }) + await webviewMessageHandler(provider, { type: "openCustomModesSettings" }) + await webviewMessageHandler(provider, { type: "openMcpSettings" }) + await webviewMessageHandler(provider, { type: "playTts", text: "hello" }) + + expect(openImage).toHaveBeenCalledWith("/test/image.png", { values: undefined }) + expect(saveImage).toHaveBeenCalledOnce() + expect(openFile).toHaveBeenCalledTimes(3) + expect(openMention).toHaveBeenCalledWith("/test/workspace", "file.ts") + expect(playTts).toHaveBeenCalledOnce() + }) + + it("covers changed configuration and rules response paths", async () => { + const provider = createProvider() + + await webviewMessageHandler(provider, { type: "getListApiConfiguration" }) + await webviewMessageHandler(provider, { type: "checkRulesDirectory", slug: "mode-1" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "listApiConfig", listApiConfig: [] }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "checkRulesDirectoryResult", + slug: "mode-1", + hasContent: true, + }) + }) + + it("covers all changed export-mode response paths", async () => { + const provider = createProvider() + const exportModeWithRules = provider.customModesManager.exportModeWithRules as ReturnType + const showSaveDialog = vi.mocked(vscode.window.showSaveDialog) + + exportModeWithRules.mockResolvedValueOnce({ success: true, yaml: "mode: one" }) + showSaveDialog.mockResolvedValueOnce({ fsPath: "/test/mode.yaml" } as vscode.Uri) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockResolvedValueOnce({ success: true, yaml: "mode: one" }) + showSaveDialog.mockResolvedValueOnce(undefined) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockResolvedValueOnce({ success: false, error: "invalid mode" }) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockRejectedValueOnce(new Error("export failed")) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "Export cancelled" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "invalid mode" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "export failed" }), + ) + }) + + it("covers all changed import-mode response paths", async () => { + const provider = createProvider() + const importModeWithRules = provider.customModesManager.importModeWithRules as ReturnType + const showOpenDialog = vi.mocked(vscode.window.showOpenDialog) + const selectedFile = [{ fsPath: "/test/mode.yaml" } as vscode.Uri] + + showOpenDialog.mockResolvedValueOnce(selectedFile) + importModeWithRules.mockResolvedValueOnce({ success: true, slug: "mode-1" }) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockResolvedValueOnce(selectedFile) + importModeWithRules.mockResolvedValueOnce({ success: false, error: "invalid mode" }) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockResolvedValueOnce(undefined) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockRejectedValueOnce(new Error("dialog failed")) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "invalid mode" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "cancelled" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "dialog failed" }), + ) + }) + + it("covers changed cloud sign-out and rate-limit error responses", async () => { + const { CloudService } = await import("@roo-code/cloud") + const { openAiCodexOAuthManager } = await import("../../../integrations/openai-codex/oauth") + const provider = createProvider() + + vi.mocked(CloudService.hasInstance).mockReturnValueOnce(false) + await webviewMessageHandler(provider, { type: "rooCloudSignOut" }) + await webviewMessageHandler(provider, { type: "rooCloudSignOut" }) + + vi.mocked(openAiCodexOAuthManager.getAccessToken).mockRejectedValueOnce(new Error("token failed")) + await webviewMessageHandler(provider, { type: "requestOpenAiCodexRateLimits" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "openAiCodexRateLimits", + error: "token failed", + }) + }) + + it("covers changed indexing status, secret, and missing-manager responses", async () => { + const manager = createIndexManager() + const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager) + const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager }) + + await webviewMessageHandler(provider, { type: "requestIndexingStatus" }) + await webviewMessageHandler(provider, { type: "requestIndexingStatus" }) + await webviewMessageHandler(provider, { type: "requestCodeIndexSecretStatus" }) + getManager.mockReturnValueOnce(undefined) + await webviewMessageHandler(provider, { type: "startIndexing" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "codeIndexSecretStatus" }), + ) + expect(provider.log).toHaveBeenCalledWith("Cannot start indexing: No workspace folder open") + }) + + it("catches both start-indexing calls during error recovery", async () => { + const manager = createIndexManager({ + isInitialized: false, + startIndexing: vi + .fn() + .mockRejectedValueOnce(new Error("first failure")) + .mockRejectedValueOnce(new Error("second failure")), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await webviewMessageHandler(provider, { type: "startIndexing" }) + await Promise.resolve() + + expect(manager.startIndexing).toHaveBeenCalledTimes(2) + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: first failure") + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: second failure") + }) + + it("covers changed stop, toggle, and detached toggle rejection paths", async () => { + const manager = createIndexManager({ + startIndexing: vi.fn().mockRejectedValue(new Error("toggle failure")), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await webviewMessageHandler(provider, { type: "stopIndexing" }) + await webviewMessageHandler(provider, { type: "toggleWorkspaceIndexing", bool: true }) + await Promise.resolve() + + expect(manager.stopIndexing).toHaveBeenCalledOnce() + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: toggle failure") + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "indexingStatusUpdate" }), + ) + }) + + it("catches auto-enabled indexing failures and posts the resulting status", async () => { + const { CodeIndexManager } = await import("../../../services/code-index/manager") + let workspaceEnabled = false + const manager = createIndexManager({ + setAutoEnableDefault: vi.fn().mockImplementation(async () => { + workspaceEnabled = true + }), + startIndexing: vi.fn().mockRejectedValue(new Error("auto-enable failure")), + }) + Object.defineProperty(manager, "isWorkspaceEnabled", { get: () => workspaceEnabled }) + const getAllInstances = vi + .spyOn(CodeIndexManager, "getAllInstances") + .mockReturnValue([manager] as unknown as ReturnType) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + try { + await webviewMessageHandler(provider, { type: "setAutoEnableDefault", bool: true }) + await Promise.resolve() + + expect(manager.startIndexing).toHaveBeenCalledOnce() + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: auto-enable failure") + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "indexingStatusUpdate" }), + ) + } finally { + getAllInstances.mockRestore() + } + }) + + it("covers changed clear-index response paths", async () => { + const manager = createIndexManager() + const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager) + const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager }) + + await webviewMessageHandler(provider, { type: "clearIndexData" }) + await webviewMessageHandler(provider, { type: "clearIndexData" }) + manager.clearIndexData.mockRejectedValueOnce(new Error("clear failed")) + await webviewMessageHandler(provider, { type: "clearIndexData" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "indexCleared", + values: { success: true }, + }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "indexCleared", + values: { success: false, error: "clear failed" }, + }) + }) + + it("covers changed marketplace error and removal responses", async () => { + const provider = createProvider() + const item = { + id: "item-1", + name: "Item 1", + description: "Test marketplace item", + type: "mode", + content: "slug: item-1", + } satisfies NonNullable + const options = { target: "project" } satisfies NonNullable + const marketplaceManager = { + installMarketplaceItem: vi.fn().mockRejectedValue(new Error("install failed")), + removeInstalledMarketplaceItem: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error("remove failed")), + } + const managerArgument = marketplaceManager as unknown as NonNullable< + Parameters[2] + > + + await webviewMessageHandler( + provider, + { type: "installMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler( + provider, + { type: "removeInstalledMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler( + provider, + { type: "removeInstalledMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler(provider, { + type: "removeInstalledMarketplaceItem", + mpItem: item, + mpInstallOptions: options, + }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceInstallResult", success: false }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", error: "remove failed" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", error: "Marketplace manager is not available" }), + ) + }) +}) + describe("Project MCP Settings", () => { let provider: ClineProvider let mockContext: vscode.ExtensionContext diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index a3f62f74f3..5a517cdcc0 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -31,7 +31,7 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore // This prevents "Current ask promise was ignored" errors // For edit operations, we don't abort because the checkpoint restore will handle it if (operation === "delete" && currentCline && !currentCline.abort) { - currentCline.abortTask() + await currentCline.abortTask() // Wait a bit for the abort to complete await pWaitFor(() => currentCline.abort === true, { timeout: 1000, diff --git a/src/core/webview/rulesMessageHandler.ts b/src/core/webview/rulesMessageHandler.ts index 343adf8738..68fcc5f7b2 100644 --- a/src/core/webview/rulesMessageHandler.ts +++ b/src/core/webview/rulesMessageHandler.ts @@ -38,7 +38,7 @@ export async function handleCreateRule( try { const input = parseCreateRuleInput(message) const createdPath = await createRule(cwd, input) - openFile(createdPath) + await openFile(createdPath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error creating rule: ${errorMessage}`) @@ -89,7 +89,7 @@ export async function handleOpenRuleFile(provider: ClineProvider, cwd: string, m throw new Error("Rule file not found") } - openFile(filePath) + await openFile(filePath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error opening rule file: ${errorMessage}`) @@ -109,7 +109,7 @@ export async function handleOpenRulesDirectory( kind: values.kind, modeSlug: values.modeSlug, } as CreateRuleInput) - openFile(directoryPath) + await openFile(directoryPath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error opening rules directory: ${errorMessage}`) diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index 496ff70c24..2eab270734 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -55,7 +55,7 @@ export async function handleCreateSkill( const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, modeSlugs) // Open the created file in the editor - openFile(createdPath) + await openFile(createdPath) // Send updated skills list const skills = skillsManager.getSkillsMetadata() @@ -199,7 +199,7 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv throw new Error(t("skills:errors.skill_not_found", { name: skillName })) } - openFile(skill.path) + await openFile(skill.path) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error opening skill file: ${errorMessage}`) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3126d4ebf5..7009343573 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -563,17 +563,21 @@ export const webviewMessageHandler = async ( const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) - provider.postStateToWebview() - provider.workspaceTracker?.initializeFilePaths() // Don't await. + await provider.postStateToWebview() + void provider.workspaceTracker + ?.initializeFilePaths() + .catch((err) => provider.log(`Workspace initialization error: ${err}`)) // Don't await. - getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })) + await getTheme().then((theme) => + provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }), + ) // If MCP Hub is already initialized, update the webview with // current server list. const mcpHub = provider.getMcpHub() if (mcpHub) { - provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() }) + await provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() }) } provider.providerSettingsManager @@ -630,7 +634,7 @@ export const webviewMessageHandler = async ( ) // Enable telemetry by default (when unset) or when explicitly enabled - provider.getStateToPostToWebview().then((state) => { + await provider.getStateToPostToWebview().then((state) => { const { telemetrySetting } = state const isOptedIn = telemetrySetting !== "disabled" TelemetryService.instance.updateTelemetryState(isOptedIn) @@ -786,7 +790,7 @@ export const webviewMessageHandler = async ( case "terminalOperation": if (message.terminalOperation) { - provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) + await provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) } break case "clearTask": @@ -812,7 +816,7 @@ export const webviewMessageHandler = async ( case "exportCurrentTask": const currentTaskId = provider.getCurrentTask()?.taskId if (currentTaskId) { - provider.exportTaskWithId(currentTaskId) + await provider.exportTaskWithId(currentTaskId) } break case "shareCurrentTask": @@ -826,13 +830,13 @@ export const webviewMessageHandler = async ( vscode.window.showErrorMessage(t("common:errors.share_not_enabled")) break case "showTaskWithId": - provider.showTaskWithId(message.text!) + await provider.showTaskWithId(message.text!) break case "condenseTaskContextRequest": - provider.condenseTaskContext(message.text!) + await provider.condenseTaskContext(message.text!) break case "deleteTaskWithId": - provider.deleteTaskWithId(message.text!) + await provider.deleteTaskWithId(message.text!) break case "abandonSubtaskWithId": provider @@ -888,7 +892,7 @@ export const webviewMessageHandler = async ( break } case "exportTaskWithId": - provider.exportTaskWithId(message.text!) + await provider.exportTaskWithId(message.text!) break case "getTaskWithAggregatedCosts": { try { @@ -1245,7 +1249,7 @@ export const webviewMessageHandler = async ( routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message. - provider.postMessageToWebview({ + void provider.postMessageToWebview({ type: "singleRouterModelFetchResponse", success: false, error: errorMessage, @@ -1254,7 +1258,7 @@ export const webviewMessageHandler = async ( } }) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "routerModels", routerModels, values: providerFilter ? { provider: requestedProvider } : undefined, @@ -1284,7 +1288,7 @@ export const webviewMessageHandler = async ( } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) provider.log(`[requestOllamaModels] Failed to refresh model cache for ${logBaseUrl}: ${errorMsg}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: {}, error: errorMsg, @@ -1297,11 +1301,11 @@ export const webviewMessageHandler = async ( // Always post a response so the webview refresh status can // transition out of "loading" — even when no models are found. - provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) provider.log(`[requestOllamaModels] Failed to read models for ${logBaseUrl}: ${errorMsg}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: {}, error: errorMsg, @@ -1329,7 +1333,7 @@ export const webviewMessageHandler = async ( } if (Object.keys(lmStudioModels).length > 0) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels: lmStudioModels, }) @@ -1341,7 +1345,7 @@ export const webviewMessageHandler = async ( break } case "requestRooModels": { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "singleRouterModelFetchResponse", success: false, error: getRouterRemovalMessage(), @@ -1357,24 +1361,24 @@ export const webviewMessageHandler = async ( message?.values?.openAiHeaders, ) - provider.postMessageToWebview({ type: "openAiModels", openAiModels }) + await provider.postMessageToWebview({ type: "openAiModels", openAiModels }) } break case "requestVsCodeLmModels": const vsCodeLmModels = await getVsCodeLmModels() // TODO: Cache like we do for OpenRouter, etc? - provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) + await provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "openImage": - openImage(message.text!, { values: message.values }) + await openImage(message.text!, { values: message.values }) break case "saveImage": if (message.dataUri) { const matches = message.dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) if (!matches) { // Let saveImage handle invalid URI error - saveImage(message.dataUri, vscode.Uri.file("")) + await saveImage(message.dataUri, vscode.Uri.file("")) break } const format = matches[1] @@ -1402,12 +1406,12 @@ export const webviewMessageHandler = async ( if (!path.isAbsolute(filePath)) { filePath = path.join(getCurrentCwd(), filePath) } - openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) + await openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) break case "readFileContent": { const relPath = message.text || "" if (!relPath) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "No path provided" }, }) @@ -1416,7 +1420,7 @@ export const webviewMessageHandler = async ( try { const cwd = getCurrentCwd() if (!cwd) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "No workspace path available" }, }) @@ -1425,17 +1429,17 @@ export const webviewMessageHandler = async ( const absPath = path.resolve(cwd, relPath) // Workspace-boundary validation: prevent path traversal attacks if (isPathOutsideWorkspace(absPath)) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "Path is outside workspace" }, }) break } const content = await fs.readFile(absPath, "utf-8") - provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content } }) + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content } }) } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: errorMsg }, }) @@ -1443,7 +1447,7 @@ export const webviewMessageHandler = async ( break } case "openMention": - openMention(getCurrentCwd(), message.text) + await openMention(getCurrentCwd(), message.text) break case "openExternal": if (message.url) { @@ -1573,7 +1577,7 @@ export const webviewMessageHandler = async ( const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() if (customModesFilePath) { - openFile(customModesFilePath) + await openFile(customModesFilePath) } break @@ -1600,7 +1604,7 @@ export const webviewMessageHandler = async ( const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath() if (mcpSettingsFilePath) { - openFile(mcpSettingsFilePath) + await openFile(mcpSettingsFilePath) } break @@ -1737,7 +1741,7 @@ export const webviewMessageHandler = async ( break case "playTts": if (message.text) { - playTts(message.text, { + void playTts(message.text, { onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }), onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }), }) @@ -1820,7 +1824,7 @@ export const webviewMessageHandler = async ( customModePrompts: updatedPrompts, hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false, } - provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) + await provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) if (TelemetryService.hasInstance()) { // Determine which setting was changed by comparing objects @@ -2225,7 +2229,7 @@ export const webviewMessageHandler = async ( try { const listApiConfig = await provider.providerSettingsManager.listConfig() await updateGlobalState("listApiConfigMeta", listApiConfig) - provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) + await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) } catch (error) { provider.log( `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -2401,7 +2405,7 @@ export const webviewMessageHandler = async ( await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8") // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: true, slug: message.slug, @@ -2411,7 +2415,7 @@ export const webviewMessageHandler = async ( vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug })) } else { // User cancelled the save dialog - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: "Export cancelled", @@ -2420,7 +2424,7 @@ export const webviewMessageHandler = async ( } } else { // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: result.error, @@ -2432,7 +2436,7 @@ export const webviewMessageHandler = async ( provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: errorMessage, @@ -2491,7 +2495,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() // Send success message to webview, include the imported slug so UI can switch - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: true, slug: result.slug, @@ -2501,7 +2505,7 @@ export const webviewMessageHandler = async ( vscode.window.showInformationMessage(t("common:info.mode_imported")) } else { // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: result.error, @@ -2512,7 +2516,7 @@ export const webviewMessageHandler = async ( } } else { // User cancelled the file dialog - reset the importing state - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: "cancelled", @@ -2523,7 +2527,7 @@ export const webviewMessageHandler = async ( provider.log(`Failed to import mode: ${errorMessage}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: errorMessage, @@ -2537,7 +2541,7 @@ export const webviewMessageHandler = async ( if (message.slug) { const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "checkRulesDirectoryResult", slug: message.slug, hasContent: hasContent, @@ -2615,14 +2619,14 @@ export const webviewMessageHandler = async ( case "rooCloudSignOut": { if (!isCloudServiceAvailable()) { await provider.postStateToWebview() - provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) + await provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) break } try { await CloudService.instance.logout() await provider.postStateToWebview() - provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) + await provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) } catch (error) { provider.log(`AuthService#logout failed: ${error}`) vscode.window.showErrorMessage("Sign out failed.") @@ -3025,7 +3029,7 @@ export const webviewMessageHandler = async ( const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", @@ -3050,7 +3054,7 @@ export const webviewMessageHandler = async ( workspacePath: undefined, } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: status, }) @@ -3070,7 +3074,7 @@ export const webviewMessageHandler = async ( )) const hasOpenRouterApiKey = !!(await provider.context.secrets.get("codebaseIndexOpenRouterApiKey")) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "codeIndexSecretStatus", values: { hasOpenAiKey, @@ -3088,7 +3092,7 @@ export const webviewMessageHandler = async ( try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", @@ -3110,12 +3114,12 @@ export const webviewMessageHandler = async ( const currentState = manager.state if (currentState === "Standby" || currentState === "Error") { - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) if (manager.state === "Standby" || manager.state === "Error") { - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } } } @@ -3133,7 +3137,7 @@ export const webviewMessageHandler = async ( return } manager.stopIndexing() - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3153,11 +3157,11 @@ export const webviewMessageHandler = async ( await manager.setWorkspaceEnabled(enabled) if (enabled && manager.isFeatureEnabled && manager.isFeatureConfigured) { await manager.initialize(provider.contextProxy) - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } else if (!enabled) { manager.stopIndexing() } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3187,10 +3191,10 @@ export const webviewMessageHandler = async ( m.stopIndexing() } else if (!wasEnabled && isNowEnabled && m.isFeatureEnabled && m.isFeatureConfigured) { await m.initialize(provider.contextProxy) - m.startIndexing() + void m.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3206,7 +3210,7 @@ export const webviewMessageHandler = async ( const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { provider.log("Cannot clear index data: No workspace folder open") - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexCleared", values: { success: false, @@ -3216,10 +3220,10 @@ export const webviewMessageHandler = async ( return } await manager.clearIndexData() - provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) + await provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) } catch (error) { provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexCleared", values: { success: false, @@ -3268,7 +3272,7 @@ export const webviewMessageHandler = async ( console.log(`Marketplace item installed and config file opened: ${configFilePath}`) // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceInstallResult", success: true, slug: message.mpItem.id, @@ -3276,7 +3280,7 @@ export const webviewMessageHandler = async ( } catch (error) { console.error(`Error installing marketplace item: ${error}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceInstallResult", success: false, error: error instanceof Error ? error.message : String(error), @@ -3294,7 +3298,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: true, slug: message.mpItem.id, @@ -3308,7 +3312,7 @@ export const webviewMessageHandler = async ( ) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: error instanceof Error ? error.message : String(error), @@ -3325,7 +3329,7 @@ export const webviewMessageHandler = async ( vscode.window.showErrorMessage(errorMessage) if (message.mpItem?.id) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: errorMessage, @@ -3441,7 +3445,7 @@ export const webviewMessageHandler = async ( const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { - openFile(command.filePath) + await openFile(command.filePath) } else { vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) } @@ -3571,7 +3575,7 @@ export const webviewMessageHandler = async ( provider.log(`Created new command file: ${filePath}`) // Open the new file in the editor - openFile(filePath) + await openFile(filePath) // Refresh commands list const { getCommands } = await import("../../services/command/commands") @@ -3695,7 +3699,7 @@ export const webviewMessageHandler = async ( const accessToken = await openAiCodexOAuthManager.getAccessToken() if (!accessToken) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", error: "Not authenticated with OpenAI Codex", }) @@ -3706,14 +3710,14 @@ export const webviewMessageHandler = async ( const { fetchOpenAiCodexRateLimitInfo } = await import("../../integrations/openai-codex/rate-limits") const rateLimits = await fetchOpenAiCodexRateLimitInfo(accessToken, { accountId }) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", values: rateLimits, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error fetching OpenAI Codex rate limits: ${errorMessage}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", error: errorMessage, }) @@ -3839,7 +3843,7 @@ export const webviewMessageHandler = async ( createNewBranch: message.worktreeCreateNewBranch, }, (progress) => { - provider.postMessageToWebview({ + void provider.postMessageToWebview({ type: "worktreeCopyProgress", copyProgressBytesCopied: progress.bytesCopied, copyProgressItemName: progress.itemName, diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index cea212fefa..65965eb8d5 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -34,7 +34,7 @@ export default [ { // Ratchet: enforce no-floating-promises directory by directory. Each // directory is added here once its floating promises are resolved. - files: ["activate/**/*.ts", "core/task/**/*.ts"], + files: ["activate/**/*.ts", "core/task/**/*.ts", "core/webview/**/*.ts"], languageOptions: { parserOptions: { project: true, From ded75751da1595590684c37753ef6493d0b313a0 Mon Sep 17 00:00:00 2001 From: rrewll Date: Wed, 29 Jul 2026 04:49:18 +0400 Subject: [PATCH 73/89] fix(branding): replace remaining user-facing "Roo" strings with "Zoo" (#971) * fix(branding): replace remaining user-facing "Roo" strings with "Zoo" Replace user-facing product references from "Roo" to "Zoo": - Webview panel and editor tab titles - Integrated terminal names - Diff view labels - VS Code LM authorization justification - LM Studio error messages - Missing-parameter tool error (now i18n-keyed via tools:missingToolParameter[WithPath] across all 16 locales) - Webview-ui localization strings (chat, settings, prompts across all locales) Per review feedback, references to the external Roo provider/router remain untouched: the routerRemoval messages, errors.roo/info.roo i18n keys, Roo credit-balance notices, and other internal identifiers that reference the legacy Roo provider rather than our brand. * test: bump coverage for Roo-to-Zoo user-facing string changes (#971) * chore: simplify i18n mocking in Task.spec.ts * test: increase coverage of vscode-lm.ts and vscode-lm-format.ts * fix(types): eliminate any casts in vscode-lm provider and format transform --------- Co-authored-by: Roomote Co-authored-by: Elliott de Launay --- .../__tests__/registerCommands.spec.ts | 60 ++- src/activate/registerCommands.ts | 2 +- src/api/providers/__tests__/lmstudio.spec.ts | 6 +- src/api/providers/__tests__/vscode-lm.spec.ts | 458 +++++++++++++++++- src/api/providers/lm-studio.ts | 4 +- src/api/providers/vscode-lm.ts | 98 ++-- .../__tests__/vscode-lm-format.spec.ts | 136 +++++- src/api/transform/vscode-lm-format.ts | 6 +- src/core/task/Task.ts | 10 +- src/core/task/__tests__/Task.spec.ts | 50 ++ src/core/webview/ClineProvider.ts | 4 +- .../webview/__tests__/ClineProvider.spec.ts | 1 + src/eslint-suppressions.json | 20 - .../__tests__/tools-missing-param.spec.ts | 46 ++ src/i18n/locales/ca/tools.json | 2 + src/i18n/locales/de/tools.json | 2 + src/i18n/locales/en/tools.json | 2 + src/i18n/locales/es/tools.json | 2 + src/i18n/locales/fr/tools.json | 2 + src/i18n/locales/hi/tools.json | 2 + src/i18n/locales/id/tools.json | 2 + src/i18n/locales/it/tools.json | 2 + src/i18n/locales/ja/tools.json | 2 + src/i18n/locales/ko/tools.json | 2 + src/i18n/locales/nl/tools.json | 2 + src/i18n/locales/pl/tools.json | 2 + src/i18n/locales/pt-BR/tools.json | 2 + src/i18n/locales/ru/tools.json | 2 + src/i18n/locales/tr/tools.json | 2 + src/i18n/locales/vi/tools.json | 2 + src/i18n/locales/zh-CN/tools.json | 2 + src/i18n/locales/zh-TW/tools.json | 2 + webview-ui/src/i18n/locales/ca/chat.json | 6 +- webview-ui/src/i18n/locales/ca/settings.json | 2 +- webview-ui/src/i18n/locales/de/chat.json | 10 +- webview-ui/src/i18n/locales/de/prompts.json | 6 +- webview-ui/src/i18n/locales/de/settings.json | 2 +- webview-ui/src/i18n/locales/es/chat.json | 6 +- webview-ui/src/i18n/locales/es/settings.json | 2 +- webview-ui/src/i18n/locales/fr/chat.json | 6 +- webview-ui/src/i18n/locales/fr/settings.json | 2 +- webview-ui/src/i18n/locales/hi/chat.json | 10 +- webview-ui/src/i18n/locales/hi/settings.json | 4 +- webview-ui/src/i18n/locales/id/chat.json | 6 +- webview-ui/src/i18n/locales/id/settings.json | 2 +- webview-ui/src/i18n/locales/it/chat.json | 6 +- webview-ui/src/i18n/locales/it/settings.json | 2 +- webview-ui/src/i18n/locales/ja/chat.json | 106 ++-- webview-ui/src/i18n/locales/ja/prompts.json | 8 +- webview-ui/src/i18n/locales/ja/settings.json | 52 +- webview-ui/src/i18n/locales/ko/chat.json | 106 ++-- webview-ui/src/i18n/locales/ko/prompts.json | 8 +- webview-ui/src/i18n/locales/ko/settings.json | 56 +-- webview-ui/src/i18n/locales/nl/chat.json | 6 +- webview-ui/src/i18n/locales/nl/settings.json | 2 +- webview-ui/src/i18n/locales/pl/chat.json | 6 +- webview-ui/src/i18n/locales/pl/settings.json | 2 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 6 +- .../src/i18n/locales/pt-BR/settings.json | 2 +- webview-ui/src/i18n/locales/ru/chat.json | 10 +- webview-ui/src/i18n/locales/ru/settings.json | 2 +- webview-ui/src/i18n/locales/tr/chat.json | 6 +- webview-ui/src/i18n/locales/tr/settings.json | 2 +- webview-ui/src/i18n/locales/vi/chat.json | 6 +- webview-ui/src/i18n/locales/vi/settings.json | 2 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 18 +- .../src/i18n/locales/zh-CN/prompts.json | 2 +- .../src/i18n/locales/zh-CN/settings.json | 4 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 2 +- 69 files changed, 1074 insertions(+), 349 deletions(-) create mode 100644 src/i18n/__tests__/tools-missing-param.spec.ts diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index fc43feae76..67a2b935ec 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -2,7 +2,7 @@ import type { Mock } from "vitest" import * as vscode from "vscode" import { ClineProvider } from "../../core/webview/ClineProvider" -import { getVisibleProviderOrLog, registerCommands, setPanel } from "../registerCommands" +import { getVisibleProviderOrLog, openClineInNewTab, registerCommands, setPanel } from "../registerCommands" vi.mock("execa", () => ({ execa: vi.fn(), @@ -13,8 +13,16 @@ vi.mock("vscode", () => ({ QuickFix: { value: "quickfix" }, RefactorRewrite: { value: "refactor.rewrite" }, }, + Uri: { + joinPath: vi.fn((_base: unknown, ..._pathSegments: string[]) => ({ path: _pathSegments.join("/") })), + }, + ViewColumn: { + Two: 2, + }, window: { createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }), + createWebviewPanel: vi.fn(), + visibleTextEditors: [], }, workspace: { workspaceFolders: [ @@ -367,3 +375,53 @@ describe("registerCommands handlers", () => { await handlers["zoo-code.plusButtonClicked"]() }) }) + +describe("openClineInNewTab", () => { + let mockOutputChannel: vscode.OutputChannel + let mockContext: vscode.ExtensionContext + + beforeEach(() => { + vi.clearAllMocks() + + mockOutputChannel = { + appendLine: vi.fn(), + append: vi.fn(), + clear: vi.fn(), + hide: vi.fn(), + name: "mock", + replace: vi.fn(), + show: vi.fn(), + dispose: vi.fn(), + } + + mockContext = { + subscriptions: [], + extensionUri: { path: "/mock/ext" }, + } as unknown as vscode.ExtensionContext + + const mockPanel = { + webview: { postMessage: vi.fn() }, + onDidChangeViewState: vi.fn(), + onDidDispose: vi.fn(), + } + ;(vscode.window.createWebviewPanel as Mock).mockReturnValue(mockPanel) + + // Reset module-level panel state. + setPanel(undefined, "sidebar") + setPanel(undefined, "tab") + }) + + it("creates a webview panel with title 'Zoo Code'", async () => { + await openClineInNewTab({ context: mockContext, outputChannel: mockOutputChannel }) + + expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith( + "zoo-code.TabPanelProvider", + "Zoo Code", + expect.any(Number), + expect.objectContaining({ + enableScripts: true, + retainContextWhenHidden: true, + }), + ) + }) +}) diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 36b7c5d153..692aabfd68 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -251,7 +251,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit { for await (const _chunk of stream) { // Should not reach here } - }).rejects.toThrow("Please check the LM Studio developer logs to debug what went wrong") + }).rejects.toThrow( + "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.", + ) }) }) @@ -144,7 +146,7 @@ describe("LmStudioHandler", () => { it("should handle API errors", async () => { mockCreate.mockRejectedValueOnce(new Error("API Error")) await expect(handler.completePrompt("Test prompt")).rejects.toThrow( - "Please check the LM Studio developer logs to debug what went wrong", + "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.", ) }) diff --git a/src/api/providers/__tests__/vscode-lm.spec.ts b/src/api/providers/__tests__/vscode-lm.spec.ts index 5227c4b289..37fb851720 100644 --- a/src/api/providers/__tests__/vscode-lm.spec.ts +++ b/src/api/providers/__tests__/vscode-lm.spec.ts @@ -12,14 +12,14 @@ vi.mock("vscode", () => { constructor( public callId: string, public name: string, - public input: any, + public input: object, ) {} } return { workspace: { getConfiguration: vi.fn(() => ({ - get: vi.fn((key: string, defaultValue: any) => defaultValue), + get: vi.fn((key: string, defaultValue: unknown) => defaultValue), })), onDidChangeConfiguration: vi.fn((_callback) => ({ dispose: vi.fn(), @@ -87,6 +87,9 @@ describe("VsCodeLmHandler", () => { beforeEach(() => { vi.clearAllMocks() + // Set up a default successful mock for selectChatModels before creating the handler + const mockModels = [{ ...mockLanguageModelChat }] + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue(mockModels) handler = new VsCodeLmHandler(defaultOptions) }) @@ -106,6 +109,14 @@ describe("VsCodeLmHandler", () => { // Should reset client when config changes expect(handler["client"]).toBeNull() }) + + it("should call initializeClient during construction", () => { + // Constructor calls initializeClient() without await, so it starts async initialization. + // Verify the handler is created and initializeClient was triggered. + expect(handler).toBeDefined() + // The constructor triggers initializeClient which calls selectChatModels + expect(vscode.lm.selectChatModels).toHaveBeenCalled() + }) }) describe("createClient", () => { @@ -135,6 +146,14 @@ describe("VsCodeLmHandler", () => { expect(client.id).toBe("default-lm") expect(client.vendor).toBe("vscode") }) + + it("should throw a Zoo Code branded error when selectChatModels fails", async () => { + ;(vscode.lm.selectChatModels as Mock).mockRejectedValueOnce(new Error("network down")) + + await expect(handler["createClient"]({ vendor: "test" })).rejects.toThrow( + "Zoo Code : Failed to select model: network down", + ) + }) }) describe("createMessage", () => { @@ -397,6 +416,338 @@ describe("VsCodeLmHandler", () => { await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error") }) + + it("should brand the LM authorization justification as Zoo Code", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user" as const, + content: "Hello", + }, + ] + + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + yield new vscode.LanguageModelTextPart("Hi") + return + })(), + text: (async function* () { + yield "Hi" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // drain + } + + expect(mockLanguageModelChat.sendRequest).toHaveBeenCalledWith( + expect.any(Array), + expect.objectContaining({ + justification: + "Zoo Code would like to use 'Test Model' from 'test-vendor', Click 'Allow' to proceed.", + }), + expect.anything(), + ) + }) + + it("should throw a Zoo Code branded error when request is cancelled", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user" as const, + content: "Hello", + }, + ] + + mockLanguageModelChat.sendRequest.mockRejectedValueOnce(new vscode.CancellationError()) + + await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow( + "Zoo Code : Request cancelled by user", + ) + }) + + it("should throw a Zoo Code branded error on stream error with error-like object", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user" as const, + content: "Hello", + }, + ] + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + mockLanguageModelChat.sendRequest.mockRejectedValueOnce({ code: "STREAM_ERROR", details: "broken" }) + + await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow( + "Zoo Code : Response stream error:", + ) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Zoo Code : Stream error object:", + expect.stringContaining("STREAM_ERROR"), + ) + + consoleErrorSpy.mockRestore() + }) + it("should log Zoo Code branded warning for unknown chunk type in stream", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + // Yield an unknown chunk type (not TextPart, not ToolCallPart) + yield { type: "unknown", foo: "bar" } as unknown as vscode.LanguageModelTextPart + return + })(), + text: (async function* () { + yield "" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // drain + } + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Unknown chunk type received:", + expect.objectContaining({ type: "unknown" }), + ) + + consoleWarnSpy.mockRestore() + }) + + it("should log Zoo Code branded warning for invalid text part value", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + // Create a TextPart with a non-string value (number) + const badTextPart = new vscode.LanguageModelTextPart(42 as unknown as string) + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + yield badTextPart + return + })(), + text: (async function* () { + yield "" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // drain + } + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Invalid text part value received:", + 42, + ) + + consoleWarnSpy.mockRestore() + }) + + it("should log Zoo Code branded warning for invalid tool callId", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + // Create a ToolCallPart with a non-string callId + const badToolCall = new vscode.LanguageModelToolCallPart(123 as unknown as string, "valid-name", {}) + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + yield badToolCall + return + })(), + text: (async function* () { + yield "" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // drain + } + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Invalid tool callId received:", + 123, + ) + + consoleWarnSpy.mockRestore() + }) + + it("should log Zoo Code branded warning for invalid tool input", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + // Create a ToolCallPart with a string input (not an object) + const badToolCall = new vscode.LanguageModelToolCallPart( + "call-1", + "valid-name", + "not-an-object" as unknown as object, + ) + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + yield badToolCall + return + })(), + text: (async function* () { + yield "" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + // drain + } + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Invalid tool input received:", + "not-an-object", + ) + + consoleWarnSpy.mockRestore() + }) + + it("should log Zoo Code branded error when tool call processing fails", async () => { + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Create a ToolCallPart with circular input that will throw on JSON.stringify + const circularInput: Record = { name: "circular" } + circularInput.self = circularInput + + const badToolCall = new vscode.LanguageModelToolCallPart("call-1", "valid-name", circularInput) + mockLanguageModelChat.sendRequest.mockResolvedValueOnce({ + stream: (async function* () { + yield badToolCall + return + })(), + text: (async function* () { + yield "" + return + })(), + }) + + const stream = handler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: [ + { + type: "function" as const, + function: { name: "test", description: "", parameters: { type: "object", properties: {} } }, + }, + ], + }) + for await (const _chunk of stream) { + // drain + } + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Zoo Code : Failed to process tool call:", + expect.any(Error), + ) + + consoleErrorSpy.mockRestore() + }) + }) + + describe("getClient", () => { + it("should log Zoo Code branded debug when creating client with selector", async () => { + const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}) + const mockModel = { ...mockLanguageModelChat } + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel]) + handler["client"] = null + + // @ts-ignore – access private method for coverage + await handler["getClient"]() + + expect(consoleDebugSpy).toHaveBeenCalledWith( + "Zoo Code : Creating client with selector:", + expect.any(Object), + ) + + consoleDebugSpy.mockRestore() + }) + + it("should throw a Zoo Code branded error when getClient fails to create client", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + ;(vscode.lm.selectChatModels as Mock).mockRejectedValueOnce(new Error("network error")) + handler["client"] = null + + // @ts-ignore – access private method for coverage + await expect(handler["getClient"]()).rejects.toThrow( + "Zoo Code : Failed to create client:", + ) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Zoo Code : Client creation failed:", + expect.stringContaining("network error"), + ) + + consoleErrorSpy.mockRestore() + }) + }) + + describe("initializeClient", () => { + it("should log when client is already initialized", async () => { + const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}) + + handler["client"] = mockLanguageModelChat + await handler.initializeClient() + + expect(consoleDebugSpy).toHaveBeenCalledWith("Zoo Code : Client already initialized") + + consoleDebugSpy.mockRestore() + }) + + it("should log success when client is initialized", async () => { + const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}) + const mockModel = { ...mockLanguageModelChat } + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel]) + handler["client"] = null + + await handler.initializeClient() + + expect(consoleDebugSpy).toHaveBeenCalledWith( + "Zoo Code : Client initialized successfully", + ) + + consoleDebugSpy.mockRestore() + }) + + it("should throw a Zoo Code branded error when client initialization fails", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + ;(vscode.lm.selectChatModels as Mock).mockRejectedValue(new Error("select failed")) + handler["client"] = null + + // Catch the unhandled rejection that may occur from the constructor's async call + const initPromise = handler.initializeClient() + + await expect(initPromise).rejects.toThrow("Zoo Code : Failed to initialize client:") + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Zoo Code : Client initialization failed:", + expect.stringContaining("select failed"), + ) + + consoleErrorSpy.mockRestore() + }) }) describe("getModel", () => { @@ -415,11 +766,18 @@ describe("VsCodeLmHandler", () => { }) it("should return fallback model info when no client exists", () => { + const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}) + // Clear the client first handler["client"] = null const model = handler.getModel() expect(model.id).toBe("test-vendor/test-family") expect(model.info).toBeDefined() + expect(consoleDebugSpy).toHaveBeenCalledWith( + "Zoo Code : No client available, using fallback model info", + ) + + consoleDebugSpy.mockRestore() }) it("should return basic model info when client exists", async () => { @@ -548,7 +906,7 @@ describe("VsCodeLmHandler", () => { cancel: vi.fn(), dispose: vi.fn(), } - handler["currentRequestCancellation"] = mockCancellation as any + handler["currentRequestCancellation"] = mockCancellation as unknown as vscode.CancellationTokenSource mockLanguageModelChat.countTokens.mockResolvedValueOnce(50) @@ -563,10 +921,17 @@ describe("VsCodeLmHandler", () => { handler["client"] = null handler["currentRequestCancellation"] = null + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }] const result = await handler.countTokens(content) expect(result).toBe(0) + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : No client available for token counting", + ) + + consoleWarnSpy.mockRestore() }) it("should handle image blocks with placeholder", async () => { @@ -581,6 +946,57 @@ describe("VsCodeLmHandler", () => { expect(result).toBe(5) expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("[IMAGE]", expect.any(Object)) }) + + it("should return 0 and log when empty text is provided to internalCountTokens", async () => { + handler["currentRequestCancellation"] = null + const consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}) + + // @ts-ignore – access private method for coverage of line 234 + const result = await handler["internalCountTokens"]("") + + expect(result).toBe(0) + expect(consoleDebugSpy).toHaveBeenCalledWith( + "Zoo Code : Empty text provided for token counting", + ) + + consoleDebugSpy.mockRestore() + }) + + it("should return 0 and log when non-numeric token count is received", async () => { + handler["currentRequestCancellation"] = null + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + mockLanguageModelChat.countTokens.mockResolvedValueOnce("not-a-number" as unknown as number) + + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "test" }] + const result = await handler.countTokens(content) + + expect(result).toBe(0) + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Non-numeric token count received:", + "not-a-number", + ) + + consoleWarnSpy.mockRestore() + }) + + it("should return 0 and log when negative token count is received", async () => { + handler["currentRequestCancellation"] = null + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + mockLanguageModelChat.countTokens.mockResolvedValueOnce(-5) + + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "test" }] + const result = await handler.countTokens(content) + + expect(result).toBe(0) + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Negative token count received:", + -5, + ) + + consoleWarnSpy.mockRestore() + }) }) describe("completePrompt", () => { @@ -622,4 +1038,40 @@ describe("VsCodeLmHandler", () => { await expect(promise).rejects.toThrow("VSCode LM completion error: Completion failed") }) }) + + describe("cleanMessageContent / deepClean", () => { + it("passes through string content unchanged", () => { + const result = handler["cleanMessageContent"]("hello") + expect(result).toBe("hello") + }) + + it("returns falsy values as-is", () => { + expect(handler["cleanMessageContent"]("")).toBe("") + }) + + it("recursively cleans array content", () => { + const input: Anthropic.Messages.MessageParam["content"] = [{ type: "text", text: "hi" }] + const result = handler["cleanMessageContent"](input) + expect(result).toEqual([{ type: "text", text: "hi" }]) + }) + + it("recursively cleans nested objects within array items", () => { + const input: Anthropic.Messages.MessageParam["content"] = [ + { type: "text", text: "hello" }, + { type: "text", text: "world" }, + ] + const result = handler["cleanMessageContent"](input) + expect(result).toEqual(input) + }) + + it("preserves primitive values other than strings inside objects", () => { + // deepClean hits the final `return value` branch for non-string primitives + // Exercise via a nested object whose property value is a number + const input = [ + { type: "text", text: "x", extra: 42 }, + ] as unknown as Anthropic.Messages.MessageParam["content"] + const result = handler["cleanMessageContent"](input) as unknown as Array> + expect(result[0].extra).toBe(42) + }) + }) }) diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index 39216418db..79f5355ef8 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -164,7 +164,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan } as const } catch (error) { throw new Error( - "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Roo Code's prompts.", + "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.", ) } } @@ -211,7 +211,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan return response.choices[0]?.message.content || "" } catch (error) { throw new Error( - "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Roo Code's prompts.", + "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Zoo Code's prompts.", ) } } diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 5c88a947de..c657e6c0d6 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -91,7 +91,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan this.dispose() throw new Error( - `Roo Code : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`, + `Zoo Code : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`, ) } } @@ -106,17 +106,17 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan try { // Check if the client is already initialized if (this.client) { - console.debug("Roo Code : Client already initialized") + console.debug("Zoo Code : Client already initialized") return } // Create a new client instance this.client = await this.createClient(this.options.vsCodeLmModelSelector || {}) - console.debug("Roo Code : Client initialized successfully") + console.debug("Zoo Code : Client initialized successfully") } catch (error) { // Handle errors during client initialization const errorMessage = error instanceof Error ? error.message : "Unknown error" - console.error("Roo Code : Client initialization failed:", errorMessage) - throw new Error(`Roo Code : Failed to initialize client: ${errorMessage}`) + console.error("Zoo Code : Client initialization failed:", errorMessage) + throw new Error(`Zoo Code : Failed to initialize client: ${errorMessage}`) } } /** @@ -164,7 +164,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error" - throw new Error(`Roo Code : Failed to select model: ${errorMessage}`) + throw new Error(`Zoo Code : Failed to select model: ${errorMessage}`) } } @@ -225,13 +225,13 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan private async internalCountTokens(text: string | vscode.LanguageModelChatMessage): Promise { // Check for required dependencies if (!this.client) { - console.warn("Roo Code : No client available for token counting") + console.warn("Zoo Code : No client available for token counting") return 0 } // Validate input if (!text) { - console.debug("Roo Code : Empty text provided for token counting") + console.debug("Zoo Code : Empty text provided for token counting") return 0 } @@ -255,24 +255,24 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } else if (text instanceof vscode.LanguageModelChatMessage) { // For chat messages, ensure we have content if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) { - console.debug("Roo Code : Empty chat message content") + console.debug("Zoo Code : Empty chat message content") return 0 } const countMessage = extractTextCountFromMessage(text) tokenCount = await this.client.countTokens(countMessage, cancellationToken) } else { - console.warn("Roo Code : Invalid input type for token counting") + console.warn("Zoo Code : Invalid input type for token counting") return 0 } // Validate the result if (typeof tokenCount !== "number") { - console.warn("Roo Code : Non-numeric token count received:", tokenCount) + console.warn("Zoo Code : Non-numeric token count received:", tokenCount) return 0 } if (tokenCount < 0) { - console.warn("Roo Code : Negative token count received:", tokenCount) + console.warn("Zoo Code : Negative token count received:", tokenCount) return 0 } @@ -280,12 +280,12 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } catch (error) { // Handle specific error types if (error instanceof vscode.CancellationError) { - console.debug("Roo Code : Token counting cancelled by user") + console.debug("Zoo Code : Token counting cancelled by user") return 0 } const errorMessage = error instanceof Error ? error.message : "Unknown error" - console.warn("Roo Code : Token counting failed:", errorMessage) + console.warn("Zoo Code : Token counting failed:", errorMessage) // Log additional error details if available if (error instanceof Error && error.stack) { @@ -317,7 +317,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan private async getClient(): Promise { if (!this.client) { - console.debug("Roo Code : Getting client with options:", { + console.debug("Zoo Code : Getting client with options:", { vsCodeLmModelSelector: this.options.vsCodeLmModelSelector, hasOptions: !!this.options, selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [], @@ -326,40 +326,46 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan try { // Use default empty selector if none provided to get all available models const selector = this.options?.vsCodeLmModelSelector || {} - console.debug("Roo Code : Creating client with selector:", selector) + console.debug("Zoo Code : Creating client with selector:", selector) this.client = await this.createClient(selector) } catch (error) { const message = error instanceof Error ? error.message : "Unknown error" - console.error("Roo Code : Client creation failed:", message) - throw new Error(`Roo Code : Failed to create client: ${message}`) + console.error("Zoo Code : Client creation failed:", message) + throw new Error(`Zoo Code : Failed to create client: ${message}`) } } return this.client } - private cleanMessageContent(content: any): any { - if (!content) { - return content + private cleanMessageContent( + content: Anthropic.Messages.MessageParam["content"], + ): Anthropic.Messages.MessageParam["content"] { + return this.deepClean(content) as Anthropic.Messages.MessageParam["content"] + } + + private deepClean(value: unknown): unknown { + if (!value) { + return value } - if (typeof content === "string") { - return content + if (typeof value === "string") { + return value } - if (Array.isArray(content)) { - return content.map((item) => this.cleanMessageContent(item)) + if (Array.isArray(value)) { + return value.map((item) => this.deepClean(item)) } - if (typeof content === "object") { - const cleaned: any = {} - for (const [key, value] of Object.entries(content)) { - cleaned[key] = this.cleanMessageContent(value) + if (typeof value === "object") { + const cleaned: Record = {} + for (const [key, v] of Object.entries(value)) { + cleaned[key] = this.deepClean(v) } return cleaned } - return content + return value } override async *createMessage( @@ -395,7 +401,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan try { // Create the response stream with required options const requestOptions: vscode.LanguageModelChatRequestOptions = { - justification: `Roo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`, + justification: `Zoo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`, tools: convertToVsCodeLmTools(metadata?.tools ?? []), } @@ -410,7 +416,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan if (chunk instanceof vscode.LanguageModelTextPart) { // Validate text part value if (typeof chunk.value !== "string") { - console.warn("Roo Code : Invalid text part value received:", chunk.value) + console.warn("Zoo Code : Invalid text part value received:", chunk.value) continue } @@ -423,23 +429,23 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan try { // Validate tool call parameters if (!chunk.name || typeof chunk.name !== "string") { - console.warn("Roo Code : Invalid tool name received:", chunk.name) + console.warn("Zoo Code : Invalid tool name received:", chunk.name) continue } if (!chunk.callId || typeof chunk.callId !== "string") { - console.warn("Roo Code : Invalid tool callId received:", chunk.callId) + console.warn("Zoo Code : Invalid tool callId received:", chunk.callId) continue } // Ensure input is a valid object if (!chunk.input || typeof chunk.input !== "object") { - console.warn("Roo Code : Invalid tool input received:", chunk.input) + console.warn("Zoo Code : Invalid tool input received:", chunk.input) continue } // Log tool call for debugging - console.debug("Roo Code : Processing tool call:", { + console.debug("Zoo Code : Processing tool call:", { name: chunk.name, callId: chunk.callId, inputSize: JSON.stringify(chunk.input).length, @@ -457,12 +463,12 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } } } catch (error) { - console.error("Roo Code : Failed to process tool call:", error) + console.error("Zoo Code : Failed to process tool call:", error) // Continue processing other chunks even if one fails continue } } else { - console.warn("Roo Code : Unknown chunk type received:", chunk) + console.warn("Zoo Code : Unknown chunk type received:", chunk) } } @@ -479,11 +485,11 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan this.ensureCleanState() if (error instanceof vscode.CancellationError) { - throw new Error("Roo Code : Request cancelled by user") + throw new Error("Zoo Code : Request cancelled by user") } if (error instanceof Error) { - console.error("Roo Code : Stream error details:", { + console.error("Zoo Code : Stream error details:", { message: error.message, stack: error.stack, name: error.name, @@ -494,13 +500,13 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } else if (typeof error === "object" && error !== null) { // Handle error-like objects const errorDetails = JSON.stringify(error, null, 2) - console.error("Roo Code : Stream error object:", errorDetails) - throw new Error(`Roo Code : Response stream error: ${errorDetails}`) + console.error("Zoo Code : Stream error object:", errorDetails) + throw new Error(`Zoo Code : Response stream error: ${errorDetails}`) } else { // Fallback for unknown error types const errorMessage = String(error) - console.error("Roo Code : Unknown stream error:", errorMessage) - throw new Error(`Roo Code : Response stream error: ${errorMessage}`) + console.error("Zoo Code : Unknown stream error:", errorMessage) + throw new Error(`Zoo Code : Response stream error: ${errorMessage}`) } } } @@ -520,7 +526,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan // Log any missing properties for debugging for (const [prop, value] of Object.entries(requiredProps)) { if (!value && value !== 0) { - console.warn(`Roo Code : Client missing ${prop} property`) + console.warn(`Zoo Code : Client missing ${prop} property`) } } @@ -551,7 +557,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan ? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector) : "vscode-lm" - console.debug("Roo Code : No client available, using fallback model info") + console.debug("Zoo Code : No client available, using fallback model info") return { id: fallbackId, diff --git a/src/api/transform/__tests__/vscode-lm-format.spec.ts b/src/api/transform/__tests__/vscode-lm-format.spec.ts index 36325e9c93..3265f2745b 100644 --- a/src/api/transform/__tests__/vscode-lm-format.spec.ts +++ b/src/api/transform/__tests__/vscode-lm-format.spec.ts @@ -16,11 +16,16 @@ interface MockLanguageModelTextPart { value: string } +type MockLanguageModelChatMessage = { + role: string + content: unknown +} + interface MockLanguageModelToolCallPart { type: "tool_call" callId: string name: string - input: any + input: object } interface MockLanguageModelToolResultPart { @@ -46,7 +51,7 @@ vitest.mock("vscode", () => { constructor( public callId: string, public name: string, - public input: any, + public input: object, ) {} } @@ -154,6 +159,61 @@ describe("convertToVsCodeLmMessages", () => { expect(toolCall.type).toBe("tool_call") }) + it("should handle tool_use with non-object non-string input", () => { + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool-num", + name: "numericTool", + input: 42 as unknown as object, // number is valid JSON + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("assistant") + // asObjectSafe returns {} for non-object/non-string, no console.warn triggered + expect(consoleWarnSpy).not.toHaveBeenCalled() + + consoleWarnSpy.mockRestore() + }) + + it("should log Zoo Code branded warning when asObjectSafe fails to parse invalid JSON string", () => { + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool-bad", + name: "badJsonTool", + input: "not-valid-json{{{", + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Zoo Code : Failed to parse object:", + expect.any(Error), + ) + + consoleWarnSpy.mockRestore() + }) + it("should handle image blocks with appropriate placeholders", () => { const messages: Anthropic.Messages.MessageParam[] = [ { @@ -186,7 +246,7 @@ describe("convertToVsCodeLmMessages", () => { content: [ { type: "image", - source: { type: "url", url: "https://example.com/img.png" } as any, + source: { type: "url", url: "https://example.com/img.png" }, }, ], }, @@ -208,7 +268,7 @@ describe("convertToVsCodeLmMessages", () => { content: [ { type: "image", - source: { type: "url", url: "https://example.com/img.png" } as any, + source: { type: "url", url: "https://example.com/img.png" }, }, ], }, @@ -217,7 +277,7 @@ describe("convertToVsCodeLmMessages", () => { ] const result = convertToVsCodeLmMessages(messages) - const toolResult = result[0].content[0] as any + const toolResult = result[0].content[0] as MockLanguageModelToolResultPart expect(toolResult.content[0].value).toContain("[Image (url): not supported by VSCode LM API]") }) @@ -241,7 +301,7 @@ describe("convertToVsCodeLmMessages", () => { ] const result = convertToVsCodeLmMessages(messages) - const toolResult = result[0].content[0] as any + const toolResult = result[0].content[0] as MockLanguageModelToolResultPart expect(toolResult.content[0].value).toBe("[Image (base64): image/jpeg not supported by VSCode LM API]") }) @@ -253,31 +313,31 @@ describe("convertToVsCodeLmMessages", () => { { type: "tool_result", tool_use_id: "tool-1", - content: [{ type: "document" } as any], + content: [{ type: "document" } as unknown as Anthropic.Messages.DocumentBlockParam], }, ], }, ] const result = convertToVsCodeLmMessages(messages) - const toolResult = result[0].content[0] as any + const toolResult = result[0].content[0] as MockLanguageModelToolResultPart expect(toolResult.content[0].value).toBe("") }) }) describe("convertToAnthropicRole", () => { it("should convert assistant role correctly", () => { - const result = convertToAnthropicRole("assistant" as any) + const result = convertToAnthropicRole(vscode.LanguageModelChatMessageRole.Assistant) expect(result).toBe("assistant") }) it("should convert user role correctly", () => { - const result = convertToAnthropicRole("user" as any) + const result = convertToAnthropicRole(vscode.LanguageModelChatMessageRole.User) expect(result).toBe("user") }) it("should return null for unknown roles", () => { - const result = convertToAnthropicRole("unknown" as any) + const result = convertToAnthropicRole("unknown" as unknown as vscode.LanguageModelChatMessageRole) expect(result).toBeNull() }) }) @@ -287,7 +347,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: "Hello world", - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("Hello world") @@ -298,7 +358,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [mockTextPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("Text content") @@ -310,7 +370,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [mockTextPart1, mockTextPart2], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("First partSecond part") @@ -324,7 +384,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [mockToolResultPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("tool-result-idTool result content") @@ -335,7 +395,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "assistant", content: [mockToolCallPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("tool-namecall-id") @@ -351,7 +411,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "assistant", content: [mockToolCallPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe(`calculatorcall-id${JSON.stringify(mockInput)}`) @@ -362,7 +422,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "assistant", content: [mockToolCallPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("tool-namecall-id") @@ -380,7 +440,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "assistant", content: [mockTextPart, mockToolResultPart, mockToolCallPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe(`Text contentresult-idTool resulttoolcall-id${JSON.stringify(mockInput)}`) @@ -390,7 +450,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("") @@ -400,7 +460,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: undefined, - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("") @@ -417,7 +477,7 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [mockToolResultPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("result-idPart 1Part 2") @@ -429,9 +489,39 @@ describe("extractTextCountFromMessage", () => { const message = { role: "user", content: [mockToolResultPart], - } as any + } satisfies MockLanguageModelChatMessage as unknown as vscode.LanguageModelChatMessage const result = extractTextCountFromMessage(message) expect(result).toBe("result-id") }) + + it("should log Zoo Code branded warning when tool call input stringify fails", () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Create an object with a circular reference that will throw on JSON.stringify + const circularInput: Record = { name: "circular" } + circularInput.self = circularInput + + const mockToolCallPart = new (vitest.mocked(vscode).LanguageModelToolCallPart)( + "call-id", + "broken-tool", + circularInput, + ) + + const message: MockLanguageModelChatMessage = { + role: "assistant", + content: [mockToolCallPart], + } + + const result = extractTextCountFromMessage(message as unknown as vscode.LanguageModelChatMessage) + + // Should still return the tool name and callId even when input stringify fails + expect(result).toBe("broken-toolcall-id") + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Zoo Code : Failed to stringify tool call input:", + expect.any(Error), + ) + + consoleErrorSpy.mockRestore() + }) }) diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index 94716b209d..7ac51e024f 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode" /** * Safely converts a value into a plain object. */ -function asObjectSafe(value: any): object { +function asObjectSafe(value: unknown): object { // Handle null/undefined if (!value) { return {} @@ -23,7 +23,7 @@ function asObjectSafe(value: any): object { return {} } catch (error) { - console.warn("Roo Code : Failed to parse object:", error) + console.warn("Zoo Code : Failed to parse object:", error) return {} } } @@ -197,7 +197,7 @@ export function extractTextCountFromMessage(message: vscode.LanguageModelChatMes try { text += JSON.stringify(item.input) } catch (error) { - console.error("Roo Code : Failed to stringify tool call input:", error) + console.error("Zoo Code : Failed to stringify tool call input:", error) } } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index da593f0553..43774eccc5 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1812,9 +1812,13 @@ export class Task extends EventEmitter implements TaskLike { async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) { await this.say( "error", - `Roo tried to use ${toolName}${ - relPath ? ` for '${relPath.toPosix()}'` : "" - } without value for required parameter '${paramName}'. Retrying...`, + relPath + ? t("tools:missingToolParameterWithPath", { + toolName, + relPath: relPath.toPosix(), + paramName, + }) + : t("tools:missingToolParameter", { toolName, paramName }), ) return formatResponse.toolError(formatResponse.missingToolParameterError(paramName)) } diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 0b4201d135..d9f7240c5c 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -187,6 +187,20 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({ vi.mock("../../ignore/RooIgnoreController") +vi.mock("../../../i18n", () => { + return { + t: (key: string, args?: Record) => { + if (key === "tools:missingToolParameterWithPath") { + return `${args?.toolName}|${args?.relPath}|${args?.paramName}` + } + if (key === "tools:missingToolParameter") { + return `${args?.toolName}|${args?.paramName}` + } + return key + }, + } +}) + vi.mock("../../condense", async (importOriginal) => { const actual = await importOriginal() return { @@ -427,6 +441,42 @@ describe("Cline", () => { }) }) + describe("sayAndCreateMissingParamError", () => { + it("surfaces a localized error notice and returns the missing-parameter tool error for both relPath branches", async () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const saySpy = vi.spyOn(cline, "say").mockResolvedValue(undefined) + + // relPath provided -> the "...WithPath" message branch. + const withPath = await cline.sayAndCreateMissingParamError("read_file", "path", "src/foo.ts") + // relPath omitted -> the plain message branch. + const withoutPath = await cline.sayAndCreateMissingParamError("execute_command", "command") + + // Both branches emit an "error" say whose resolved text names the tool and the + // missing parameter (guards against a silent i18n regression where t() would + // otherwise return the raw key or an empty string and still type-check as a String). + expect(saySpy).toHaveBeenCalledTimes(2) + const [withPathChannel, withPathNotice] = saySpy.mock.calls[0] + const [withoutPathChannel, withoutPathNotice] = saySpy.mock.calls[1] + expect(withPathChannel).toBe("error") + expect(withoutPathChannel).toBe("error") + expect(withPathNotice).toEqual(expect.stringContaining("read_file")) + expect(withPathNotice).toEqual(expect.stringContaining("path")) + expect(withPathNotice).toEqual(expect.stringContaining("src/foo.ts")) + expect(withoutPathNotice).toEqual(expect.stringContaining("execute_command")) + expect(withoutPathNotice).toEqual(expect.stringContaining("command")) + + // The returned tool error names the missing parameter. + expect(withPath).toContain("path") + expect(withoutPath).toContain("command") + }) + }) + describe("getEnvironmentDetails", () => { describe("API conversation handling", () => { it("should strip non-protocol fields from API conversation history before sending to the API", async () => { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7bdf03f0a2..28870fc446 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1379,7 +1379,7 @@ export class ClineProvider window.AUDIO_BASE_URI = "${audioUri}" window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}" - Roo Code + Zoo Code
@@ -1458,7 +1458,7 @@ export class ClineProvider window.AUDIO_BASE_URI = "${audioUri}" window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}" - Roo Code + Zoo Code diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 1e2c0402bf..b99e502f61 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -614,6 +614,7 @@ describe("ClineProvider", () => { }) expect(mockWebviewView.webview.html).toContain("") + expect(mockWebviewView.webview.html).toContain("Zoo Code") }) describe("logWebviewHiddenDiagnostics", () => { diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 4b0f48253f..1f5e90a12d 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -289,11 +289,6 @@ "count": 5 } }, - "api/providers/__tests__/vscode-lm.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, "api/providers/__tests__/xai.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -479,11 +474,6 @@ "count": 7 } }, - "api/providers/vscode-lm.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, "api/providers/xai.ts": { "@typescript-eslint/no-explicit-any": { "count": 7 @@ -544,11 +534,6 @@ "count": 5 } }, - "api/transform/__tests__/vscode-lm-format.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 23 - } - }, "api/transform/__tests__/zai-format.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 3 @@ -599,11 +584,6 @@ "count": 4 } }, - "api/transform/vscode-lm-format.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "api/transform/zai-format.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 diff --git a/src/i18n/__tests__/tools-missing-param.spec.ts b/src/i18n/__tests__/tools-missing-param.spec.ts new file mode 100644 index 0000000000..f164bf06be --- /dev/null +++ b/src/i18n/__tests__/tools-missing-param.spec.ts @@ -0,0 +1,46 @@ +import fs from "fs" +import path from "path" + +const localesDir = path.join(__dirname, "..", "locales") + +const languages = fs + .readdirSync(localesDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith(".")) + .map((dirent) => dirent.name) + +function loadTools(language: string): Record { + return JSON.parse(fs.readFileSync(path.join(localesDir, language, "tools.json"), "utf8")) +} + +describe("tools:missingToolParameter locales", () => { + it("covers every shipped locale", () => { + expect(languages.length).toBeGreaterThan(0) + for (const language of languages) { + const tools = loadTools(language) + expect(tools.missingToolParameter, `missingToolParameter missing in ${language}`).toBeTruthy() + expect( + tools.missingToolParameterWithPath, + `missingToolParameterWithPath missing in ${language}`, + ).toBeTruthy() + } + }) + + it("keeps all interpolation placeholders in every locale", () => { + for (const language of languages) { + const tools = loadTools(language) + expect(tools.missingToolParameter, `placeholders missing in ${language}`).toContain("{{toolName}}") + expect(tools.missingToolParameter, `placeholders missing in ${language}`).toContain("{{paramName}}") + expect(tools.missingToolParameterWithPath, `placeholders missing in ${language}`).toContain("{{toolName}}") + expect(tools.missingToolParameterWithPath, `placeholders missing in ${language}`).toContain("{{relPath}}") + expect(tools.missingToolParameterWithPath, `placeholders missing in ${language}`).toContain("{{paramName}}") + } + }) + + it("brands the English messages as Zoo, not Roo", () => { + const tools = loadTools("en") + expect(tools.missingToolParameter).toContain("Zoo") + expect(tools.missingToolParameter).not.toContain("Roo") + expect(tools.missingToolParameterWithPath).toContain("Zoo") + expect(tools.missingToolParameterWithPath).not.toContain("Roo") + }) +}) diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 223264252e..93f363cf25 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "unknownToolError": "Zoo ha intentat utilitzar una eina desconeguda: \"{{toolName}}\". Reintentant...", + "missingToolParameter": "Zoo ha intentat utilitzar {{toolName}} sense valor per al paràmetre obligatori '{{paramName}}'. Reintentant...", + "missingToolParameterWithPath": "Zoo ha intentat utilitzar {{toolName}} per a '{{relPath}}' sense valor per al paràmetre obligatori '{{paramName}}'. Reintentant...", "codebaseSearch": { "approval": "Cercant '{{query}}' a la base de codi..." }, diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index d04362f2d9..c1f358a01e 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Zoo zu einem anderen Ansatz zu führen.", "unknownToolError": "Zoo hat versucht, ein unbekanntes Tool zu verwenden: \"{{toolName}}\". Wiederhole Versuch...", + "missingToolParameter": "Zoo hat versucht, {{toolName}} ohne Wert für den erforderlichen Parameter '{{paramName}}' zu verwenden. Wiederhole Versuch...", + "missingToolParameterWithPath": "Zoo hat versucht, {{toolName}} für '{{relPath}}' ohne Wert für den erforderlichen Parameter '{{paramName}}' zu verwenden. Wiederhole Versuch...", "codebaseSearch": { "approval": "Suche nach '{{query}}' im Codebase..." }, diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 9c08368e53..acde8f23bd 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "unknownToolError": "Zoo tried to use an unknown tool: \"{{toolName}}\". Retrying...", + "missingToolParameter": "Zoo tried to use {{toolName}} without value for required parameter '{{paramName}}'. Retrying...", + "missingToolParameterWithPath": "Zoo tried to use {{toolName}} for '{{relPath}}' without value for required parameter '{{paramName}}'. Retrying...", "codebaseSearch": { "approval": "Searching for '{{query}}' in codebase..." }, diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index f865a8e484..48d6e4da39 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "unknownToolError": "Zoo intentó usar una herramienta desconocida: \"{{toolName}}\". Reintentando...", + "missingToolParameter": "Zoo intentó usar {{toolName}} sin valor para el parámetro requerido '{{paramName}}'. Reintentando...", + "missingToolParameterWithPath": "Zoo intentó usar {{toolName}} para '{{relPath}}' sin valor para el parámetro requerido '{{paramName}}'. Reintentando...", "codebaseSearch": { "approval": "Buscando '{{query}}' en la base de código..." }, diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index 7dd65facc4..45d6ba3325 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "unknownToolError": "Zoo a tenté d'utiliser un outil inconnu : \"{{toolName}}\". Nouvelle tentative...", + "missingToolParameter": "Zoo a tenté d'utiliser {{toolName}} sans valeur pour le paramètre requis '{{paramName}}'. Nouvelle tentative...", + "missingToolParameterWithPath": "Zoo a tenté d'utiliser {{toolName}} pour '{{relPath}}' sans valeur pour le paramètre requis '{{paramName}}'. Nouvelle tentative...", "codebaseSearch": { "approval": "Recherche de '{{query}}' dans la base de code..." }, diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 04193d45a0..e377c46f09 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "unknownToolError": "Zoo ने एक अज्ञात उपकरण का उपयोग करने का प्रयास किया: \"{{toolName}}\"। पुनः प्रयास कर रहा है...", + "missingToolParameter": "Zoo ने आवश्यक पैरामीटर '{{paramName}}' के मान के बिना {{toolName}} का उपयोग करने का प्रयास किया। पुनः प्रयास कर रहा है...", + "missingToolParameterWithPath": "Zoo ने '{{relPath}}' के लिए आवश्यक पैरामीटर '{{paramName}}' के मान के बिना {{toolName}} का उपयोग करने का प्रयास किया। पुनः प्रयास कर रहा है...", "codebaseSearch": { "approval": "कोडबेस में '{{query}}' खोज रहा है..." }, diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index b6b64fdcae..04d1d16daa 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.", "unknownToolError": "Zoo mencoba menggunakan alat yang tidak dikenal: \"{{toolName}}\". Mencoba lagi...", + "missingToolParameter": "Zoo mencoba menggunakan {{toolName}} tanpa nilai untuk parameter wajib '{{paramName}}'. Mencoba lagi...", + "missingToolParameterWithPath": "Zoo mencoba menggunakan {{toolName}} untuk '{{relPath}}' tanpa nilai untuk parameter wajib '{{paramName}}'. Mencoba lagi...", "codebaseSearch": { "approval": "Mencari '{{query}}' di codebase..." }, diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index 97d851bd7f..e384a0ea80 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "unknownToolError": "Zoo ha provato ad utilizzare uno strumento sconosciuto: \"{{toolName}}\". Nuovo tentativo...", + "missingToolParameter": "Zoo ha provato a utilizzare {{toolName}} senza valore per il parametro obbligatorio '{{paramName}}'. Nuovo tentativo...", + "missingToolParameterWithPath": "Zoo ha provato a utilizzare {{toolName}} per '{{relPath}}' senza valore per il parametro obbligatorio '{{paramName}}'. Nuovo tentativo...", "codebaseSearch": { "approval": "Ricerca di '{{query}}' nella base di codice..." }, diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index 3987b7b745..f319b0b768 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "unknownToolError": "Zooが不明なツールを使用しようとしました:「{{toolName}}」。再試行中...", + "missingToolParameter": "Zooが必須パラメータ '{{paramName}}' の値なしで {{toolName}} を使用しようとしました。再試行中...", + "missingToolParameterWithPath": "Zooが '{{relPath}}' に対して必須パラメータ '{{paramName}}' の値なしで {{toolName}} を使用しようとしました。再試行中...", "codebaseSearch": { "approval": "コードベースで '{{query}}' を検索中..." }, diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index cc65e2a80b..6b8e91726b 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "unknownToolError": "Zoo가 알 수 없는 도구를 사용하려고 했습니다: \"{{toolName}}\". 다시 시도 중...", + "missingToolParameter": "Zoo가 필수 매개변수 '{{paramName}}'의 값 없이 {{toolName}}을(를) 사용하려고 했습니다. 다시 시도 중...", + "missingToolParameterWithPath": "Zoo가 '{{relPath}}'에 대해 필수 매개변수 '{{paramName}}'의 값 없이 {{toolName}}을(를) 사용하려고 했습니다. 다시 시도 중...", "codebaseSearch": { "approval": "코드베이스에서 '{{query}}' 검색 중..." }, diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index d157275a7a..5a5df015a7 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Zoo naar een andere aanpak te leiden.", "unknownToolError": "Zoo probeerde een onbekende tool te gebruiken: \"{{toolName}}\". Opnieuw proberen...", + "missingToolParameter": "Zoo probeerde {{toolName}} te gebruiken zonder waarde voor de vereiste parameter '{{paramName}}'. Opnieuw proberen...", + "missingToolParameterWithPath": "Zoo probeerde {{toolName}} te gebruiken voor '{{relPath}}' zonder waarde voor de vereiste parameter '{{paramName}}'. Opnieuw proberen...", "codebaseSearch": { "approval": "Zoeken naar '{{query}}' in codebase..." }, diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index fce837aafb..32a3e27f49 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Wygląda na to, że Zoo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "unknownToolError": "Zoo próbował użyć nieznanego narzędzia: \"{{toolName}}\". Ponowna próba...", + "missingToolParameter": "Zoo próbował użyć {{toolName}} bez wartości dla wymaganego parametru '{{paramName}}'. Ponowna próba...", + "missingToolParameterWithPath": "Zoo próbował użyć {{toolName}} dla '{{relPath}}' bez wartości dla wymaganego parametru '{{paramName}}'. Ponowna próba...", "codebaseSearch": { "approval": "Wyszukiwanie '{{query}}' w bazie kodu..." }, diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 6505067607..28e950db9c 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "unknownToolError": "Zoo tentou usar uma ferramenta desconhecida: \"{{toolName}}\". Tentando novamente...", + "missingToolParameter": "Zoo tentou usar {{toolName}} sem valor para o parâmetro obrigatório '{{paramName}}'. Tentando novamente...", + "missingToolParameterWithPath": "Zoo tentou usar {{toolName}} para '{{relPath}}' sem valor para o parâmetro obrigatório '{{paramName}}'. Tentando novamente...", "codebaseSearch": { "approval": "Pesquisando '{{query}}' na base de código..." }, diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 6fecfc272b..6d20010a45 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Похоже, что Zoo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "unknownToolError": "Zoo попытался использовать неизвестный инструмент: \"{{toolName}}\". Повторная попытка...", + "missingToolParameter": "Zoo попытался использовать {{toolName}} без значения для обязательного параметра '{{paramName}}'. Повторная попытка...", + "missingToolParameterWithPath": "Zoo попытался использовать {{toolName}} для '{{relPath}}' без значения для обязательного параметра '{{paramName}}'. Повторная попытка...", "codebaseSearch": { "approval": "Поиск '{{query}}' в кодовой базе..." }, diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index d2a79a9fab..de447fcf21 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "unknownToolError": "Zoo bilinmeyen bir araç kullanmaya çalıştı: \"{{toolName}}\". Yeniden deneniyor...", + "missingToolParameter": "Zoo, gerekli '{{paramName}}' parametresi için değer olmadan {{toolName}} kullanmaya çalıştı. Yeniden deneniyor...", + "missingToolParameterWithPath": "Zoo, '{{relPath}}' için gerekli '{{paramName}}' parametresi için değer olmadan {{toolName}} kullanmaya çalıştı. Yeniden deneniyor...", "codebaseSearch": { "approval": "Kod tabanında '{{query}}' aranıyor..." }, diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 96c0174317..ec43f2b39e 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Zoo theo một cách tiếp cận khác.", "unknownToolError": "Zoo đã cố gắng sử dụng một công cụ không xác định: \"{{toolName}}\". Đang thử lại...", + "missingToolParameter": "Zoo đã cố gắng sử dụng {{toolName}} mà không có giá trị cho tham số bắt buộc '{{paramName}}'. Đang thử lại...", + "missingToolParameterWithPath": "Zoo đã cố gắng sử dụng {{toolName}} cho '{{relPath}}' mà không có giá trị cho tham số bắt buộc '{{paramName}}'. Đang thử lại...", "codebaseSearch": { "approval": "Đang tìm kiếm '{{query}}' trong cơ sở mã..." }, diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 9a16e15ac5..d5a5701f3d 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "unknownToolError": "Zoo 尝试使用未知工具:\"{{toolName}}\"。正在重试...", + "missingToolParameter": "Zoo 尝试使用 {{toolName}} 但未提供必需参数 '{{paramName}}' 的值。正在重试...", + "missingToolParameterWithPath": "Zoo 尝试对 '{{relPath}}' 使用 {{toolName}} 但未提供必需参数 '{{paramName}}' 的值。正在重试...", "codebaseSearch": { "approval": "正在搜索代码库中的 '{{query}}'..." }, diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 0d35605211..02623543b9 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -8,6 +8,8 @@ }, "toolRepetitionLimitReached": "Zoo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "unknownToolError": "Zoo 嘗試使用未知工具:「{{toolName}}」。正在重試...", + "missingToolParameter": "Zoo 嘗試使用 {{toolName}} 但未提供必要參數 '{{paramName}}' 的值。正在重試...", + "missingToolParameterWithPath": "Zoo 嘗試對 '{{relPath}}' 使用 {{toolName}} 但未提供必要參數 '{{paramName}}' 的值。正在重試...", "codebaseSearch": { "approval": "正在搜尋程式碼庫中的「{{query}}」..." }, diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index d4e5e2524e..11ac1d4028 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -187,7 +187,7 @@ "current": "Actual" }, "instructions": { - "wantsToFetch": "Roo vol obtenir instruccions detallades per ajudar amb la tasca actual." + "wantsToFetch": "Zoo vol obtenir instruccions detallades per ajudar amb la tasca actual." }, "fileOperations": { "wantsToRead": "Zoo vol llegir aquest fitxer", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo vol editar aquest fitxer fora de l'espai de treball", "wantsToEditProtected": "Zoo vol editar un fitxer de configuració protegit", "wantsToCreate": "Zoo vol crear un nou fitxer", - "wantsToSearchReplace": "Roo vol realitzar cerca i substitució en aquest fitxer", - "didSearchReplace": "Roo ha realitzat cerca i substitució en aquest fitxer", + "wantsToSearchReplace": "Zoo vol realitzar cerca i substitució en aquest fitxer", + "didSearchReplace": "Zoo ha realitzat cerca i substitució en aquest fitxer", "wantsToInsert": "Zoo vol inserir contingut en aquest fitxer", "wantsToInsertWithLineNumber": "Zoo vol inserir contingut a la línia {{lineNumber}} d'aquest fitxer", "wantsToInsertAtEnd": "Zoo vol afegir contingut al final d'aquest fitxer", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index f861b8734c..b9644e0d12 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Pots configurar una drecera global per a aquesta configuració a les preferències del teu IDE.", - "description": "Permet que Roo realitzi operacions automàticament sense requerir aprovació. Activeu aquesta configuració només si confieu plenament en la IA i enteneu els riscos de seguretat associats.", + "description": "Permet que Zoo realitzi operacions automàticament sense requerir aprovació. Activeu aquesta configuració només si confieu plenament en la IA i enteneu els riscos de seguretat associats.", "enabled": "Auto-aprovació activada", "toggleAriaLabel": "Commuta l'aprovació automàtica", "disabledAriaLabel": "Aprovació automàtica desactivada: seleccioneu primer les opcions", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index fb63e5b6a1..38415c87a5 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -130,7 +130,7 @@ "title": "Modi", "marketplace": "Modus-Marketplace", "settings": "Modus-Einstellungen", - "description": "Spezialisierte Personas, die Roos Verhalten anpassen.", + "description": "Spezialisierte Personas, die Zoos Verhalten anpassen.", "searchPlaceholder": "Modi suchen...", "noResults": "Keine Ergebnisse gefunden" }, @@ -187,7 +187,7 @@ "current": "Aktuell" }, "instructions": { - "wantsToFetch": "Roo möchte detaillierte Anweisungen abrufen, um bei der aktuellen Aufgabe zu helfen" + "wantsToFetch": "Zoo möchte detaillierte Anweisungen abrufen, um bei der aktuellen Aufgabe zu helfen" }, "fileOperations": { "wantsToRead": "Zoo möchte diese Datei lesen", @@ -198,8 +198,8 @@ "wantsToEditOutsideWorkspace": "Zoo möchte diese Datei außerhalb des Arbeitsbereichs bearbeiten", "wantsToEditProtected": "Zoo möchte eine geschützte Konfigurationsdatei bearbeiten", "wantsToCreate": "Zoo möchte eine neue Datei erstellen", - "wantsToSearchReplace": "Roo möchte in dieser Datei suchen und ersetzen", - "didSearchReplace": "Roo hat Suchen und Ersetzen in dieser Datei durchgeführt", + "wantsToSearchReplace": "Zoo möchte in dieser Datei suchen und ersetzen", + "didSearchReplace": "Zoo hat Suchen und Ersetzen in dieser Datei durchgeführt", "wantsToInsert": "Zoo möchte Inhalte in diese Datei einfügen", "wantsToInsertWithLineNumber": "Zoo möchte Inhalte in diese Datei in Zeile {{lineNumber}} einfügen", "wantsToInsertAtEnd": "Zoo möchte Inhalte am Ende dieser Datei anhängen", @@ -288,7 +288,7 @@ "copyToClipboard": "In Zwischenablage kopieren", "copied": "Kopiert!", "diagnostics": "Detaillierte Fehlerinformationen abrufen", - "proxyProvider": "Du scheinst einen Proxy-basierten Anbieter zu verwenden. Überprüfe unbedingt seine Protokolle und stelle sicher, dass er Roos Anfragen nicht umschreibt." + "proxyProvider": "Du scheinst einen Proxy-basierten Anbieter zu verwenden. Überprüfe unbedingt seine Protokolle und stelle sicher, dass er Zoos Anfragen nicht umschreibt." }, "powershell": { "issues": "Es scheint, dass du Probleme mit Windows PowerShell hast, bitte sieh dir dies an" diff --git a/webview-ui/src/i18n/locales/de/prompts.json b/webview-ui/src/i18n/locales/de/prompts.json index 551c217125..28f7cbec5f 100644 --- a/webview-ui/src/i18n/locales/de/prompts.json +++ b/webview-ui/src/i18n/locales/de/prompts.json @@ -9,7 +9,7 @@ "editModesConfig": "Moduskonfiguration bearbeiten", "editGlobalModes": "Globale Modi bearbeiten", "editProjectModes": "Projektmodi bearbeiten (.roomodes)", - "createModeHelpText": "Modi sind spezialisierte Personas, die Roos Verhalten anpassen. <0>Erfahre mehr über die Verwendung von Modi oder <1>die Anpassung von Modi.", + "createModeHelpText": "Modi sind spezialisierte Personas, die Zoos Verhalten anpassen. <0>Erfahre mehr über die Verwendung von Modi oder <1>die Anpassung von Modi.", "selectMode": "Modi suchen" }, "apiConfiguration": { @@ -33,7 +33,7 @@ "roleDefinition": { "title": "Rollendefinition", "resetToDefault": "Auf Standardwerte zurücksetzen", - "description": "Definiere Roos Expertise und Persönlichkeit für diesen Modus. Diese Beschreibung prägt, wie Zoo sich präsentiert und an Aufgaben herangeht." + "description": "Definiere Zoos Expertise und Persönlichkeit für diesen Modus. Diese Beschreibung prägt, wie Zoo sich präsentiert und an Aufgaben herangeht." }, "description": { "title": "Kurzbeschreibung (für Menschen)", @@ -167,7 +167,7 @@ }, "roleDefinition": { "label": "Rollendefinition", - "description": "Definiere Roos Expertise und Persönlichkeit für diesen Modus." + "description": "Definiere Zoos Expertise und Persönlichkeit für diesen Modus." }, "whenToUse": { "label": "Wann zu verwenden (optional)", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index e1ff092237..46a8bd28e4 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Du kannst in deinen IDE-Einstellungen einen globalen Shortcut für diese Einstellung konfigurieren.", - "description": "Erlaubt Roo, Operationen automatisch ohne Genehmigung durchzuführen. Aktiviere diese Einstellungen nur, wenn du der KI vollständig vertraust und die damit verbundenen Sicherheitsrisiken verstehst.", + "description": "Erlaubt Zoo, Operationen automatisch ohne Genehmigung durchzuführen. Aktiviere diese Einstellungen nur, wenn du der KI vollständig vertraust und die damit verbundenen Sicherheitsrisiken verstehst.", "enabled": "Auto-Genehmigung aktiviert", "toggleAriaLabel": "Automatische Genehmigung umschalten", "disabledAriaLabel": "Automatische Genehmigung deaktiviert - zuerst Optionen auswählen", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 1a297de709..d3beb38fe0 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -187,7 +187,7 @@ "current": "Actual" }, "instructions": { - "wantsToFetch": "Roo quiere obtener instrucciones detalladas para ayudar con la tarea actual" + "wantsToFetch": "Zoo quiere obtener instrucciones detalladas para ayudar con la tarea actual" }, "fileOperations": { "wantsToRead": "Zoo quiere leer este archivo", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo quiere editar este archivo fuera del espacio de trabajo", "wantsToEditProtected": "Zoo quiere editar un archivo de configuración protegido", "wantsToCreate": "Zoo quiere crear un nuevo archivo", - "wantsToSearchReplace": "Roo quiere realizar búsqueda y reemplazo en este archivo", - "didSearchReplace": "Roo realizó búsqueda y reemplazo en este archivo", + "wantsToSearchReplace": "Zoo quiere realizar búsqueda y reemplazo en este archivo", + "didSearchReplace": "Zoo realizó búsqueda y reemplazo en este archivo", "wantsToInsert": "Zoo quiere insertar contenido en este archivo", "wantsToInsertWithLineNumber": "Zoo quiere insertar contenido en este archivo en la línea {{lineNumber}}", "wantsToInsertAtEnd": "Zoo quiere añadir contenido al final de este archivo", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 4a5ce59d79..f9c1be76f4 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Puedes configurar un atajo global para esta configuración en las preferencias de tu IDE.", - "description": "Permitir que Roo realice operaciones automáticamente sin requerir aprobación. Habilite esta configuración solo si confía plenamente en la IA y comprende los riesgos de seguridad asociados.", + "description": "Permitir que Zoo realice operaciones automáticamente sin requerir aprobación. Habilite esta configuración solo si confía plenamente en la IA y comprende los riesgos de seguridad asociados.", "enabled": "Auto-aprobación habilitada", "toggleAriaLabel": "Alternar aprobación automática", "disabledAriaLabel": "Aprobación automática desactivada: seleccione primero las opciones", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 24d27418e7..080e3cb044 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -194,8 +194,8 @@ "wantsToEditOutsideWorkspace": "Zoo veut éditer ce fichier en dehors de l'espace de travail", "wantsToEditProtected": "Zoo veut éditer un fichier de configuration protégé", "wantsToCreate": "Zoo veut créer un nouveau fichier", - "wantsToSearchReplace": "Roo veut effectuer une recherche et remplacement sur ce fichier", - "didSearchReplace": "Roo a effectué une recherche et remplacement sur ce fichier", + "wantsToSearchReplace": "Zoo veut effectuer une recherche et remplacement sur ce fichier", + "didSearchReplace": "Zoo a effectué une recherche et remplacement sur ce fichier", "wantsToInsert": "Zoo veut insérer du contenu dans ce fichier", "wantsToInsertWithLineNumber": "Zoo veut insérer du contenu dans ce fichier à la ligne {{lineNumber}}", "wantsToInsertAtEnd": "Zoo veut ajouter du contenu à la fin de ce fichier", @@ -208,7 +208,7 @@ "didGenerateImage": "Zoo a généré une image" }, "instructions": { - "wantsToFetch": "Roo veut récupérer des instructions détaillées pour aider à la tâche actuelle" + "wantsToFetch": "Zoo veut récupérer des instructions détaillées pour aider à la tâche actuelle" }, "directoryOperations": { "wantsToViewTopLevel": "Zoo veut voir les fichiers de premier niveau dans ce répertoire", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 03c64cc1e9..6da211144b 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Vous pouvez configurer un raccourci global pour ce paramètre dans les préférences de votre IDE.", - "description": "Permettre à Roo d'effectuer automatiquement des opérations sans requérir d'approbation. Activez ces paramètres uniquement si vous faites entièrement confiance à l'IA et que vous comprenez les risques de sécurité associés.", + "description": "Permettre à Zoo d'effectuer automatiquement des opérations sans requérir d'approbation. Activez ces paramètres uniquement si vous faites entièrement confiance à l'IA et que vous comprenez les risques de sécurité associés.", "enabled": "Auto-approbation activée", "toggleAriaLabel": "Activer/désactiver l'approbation automatique", "disabledAriaLabel": "Approbation automatique désactivée - sélectionnez d'abord les options", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 65306b39e8..f2ac1cb20d 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -187,7 +187,7 @@ "current": "वर्तमान" }, "instructions": { - "wantsToFetch": "Roo को वर्तमान कार्य में सहायता के लिए विस्तृत निर्देश प्राप्त करना है" + "wantsToFetch": "Zoo को वर्तमान कार्य में सहायता के लिए विस्तृत निर्देश प्राप्त करना है" }, "fileOperations": { "wantsToRead": "Zoo इस फ़ाइल को पढ़ना चाहता है", @@ -197,12 +197,12 @@ "wantsToEditOutsideWorkspace": "Zoo कार्यक्षेत्र के बाहर इस फ़ाइल को संपादित करना चाहता है", "wantsToEditProtected": "Zoo एक सुरक्षित कॉन्फ़िगरेशन फ़ाइल को संपादित करना चाहता है", "wantsToCreate": "Zoo एक नई फ़ाइल बनाना चाहता है", - "wantsToSearchReplace": "Roo इस फ़ाइल में खोज और प्रतिस्थापन करना चाहता है", - "didSearchReplace": "Roo ने इस फ़ाइल में खोज और प्रतिस्थापन किया", + "wantsToSearchReplace": "Zoo इस फ़ाइल में खोज और प्रतिस्थापन करना चाहता है", + "didSearchReplace": "Zoo ने इस फ़ाइल में खोज और प्रतिस्थापन किया", "wantsToInsert": "Zoo इस फ़ाइल में सामग्री डालना चाहता है", "wantsToInsertWithLineNumber": "Zoo इस फ़ाइल की {{lineNumber}} लाइन पर सामग्री डालना चाहता है", "wantsToInsertAtEnd": "Zoo इस फ़ाइल के अंत में सामग्री जोड़ना चाहता है", - "wantsToReadAndXMore": "रू इस फ़ाइल को और {{count}} अन्य को पढ़ना चाहता है", + "wantsToReadAndXMore": "Zoo इस फ़ाइल को और {{count}} अन्य को पढ़ना चाहता है", "wantsToReadMultiple": "Zoo कई फ़ाइलें पढ़ना चाहता है", "wantsToApplyBatchChanges": "Zoo कई फ़ाइलों में परिवर्तन लागू करना चाहता है", "wantsToGenerateImage": "Zoo एक छवि बनाना चाहता है", @@ -246,7 +246,7 @@ "response": "प्रतिक्रिया", "arguments": "आर्ग्युमेंट्स", "text": { - "rooSaid": "रू ने कहा" + "rooSaid": "Zoo ने कहा" }, "feedback": { "youSaid": "आपने कहा" diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2f6c6fc542..522c68a586 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "आप अपनी आईडीई वरीयताओं में इस सेटिंग के लिए एक वैश्विक शॉर्टकट कॉन्फ़िगर कर सकते हैं।", - "description": "Roo को अनुमोदन की आवश्यकता के बिना स्वचालित रूप से ऑपरेशन करने की अनुमति दें। इन सेटिंग्स को केवल तभी सक्षम करें जब आप AI पर पूरी तरह से भरोसा करते हों और संबंधित सुरक्षा जोखिमों को समझते हों।", + "description": "Zoo को अनुमोदन की आवश्यकता के बिना स्वचालित रूप से ऑपरेशन करने की अनुमति दें। इन सेटिंग्स को केवल तभी सक्षम करें जब आप AI पर पूरी तरह से भरोसा करते हों और संबंधित सुरक्षा जोखिमों को समझते हों।", "enabled": "स्वत:-अनुमोदन सक्षम", "toggleAriaLabel": "स्वतः-अनुमोदन टॉगल करें", "disabledAriaLabel": "स्वतः-अनुमोदन अक्षम - पहले विकल्प चुनें", @@ -616,7 +616,7 @@ }, "consecutiveMistakeLimit": { "label": "त्रुटि और पुनरावृत्ति सीमा", - "description": "'रू को समस्या हो रही है' संवाद दिखाने से पहले लगातार त्रुटियों या दोहराए गए कार्यों की संख्या। इस सुरक्षा तंत्र को अक्षम करने के लिए 0 पर सेट करें (यह कभी ट्रिगर नहीं होगा)।", + "description": "'Zoo को समस्या हो रही है' संवाद दिखाने से पहले लगातार त्रुटियों या दोहराए गए कार्यों की संख्या। इस सुरक्षा तंत्र को अक्षम करने के लिए 0 पर सेट करें (यह कभी ट्रिगर नहीं होगा)।", "unlimitedDescription": "असीमित पुनः प्रयास सक्षम (स्वतः आगे बढ़ें)। संवाद कभी नहीं दिखाई देगा।", "warning": "⚠️ 0 पर सेट करने से असीमित पुनः प्रयास की अनुमति मिलती है जिससे महत्वपूर्ण एपीआई उपयोग हो सकता है" }, diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index fb11881cb4..e95a517221 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -231,7 +231,7 @@ "didLoad": "Zoo telah memuat keterampilan" }, "instructions": { - "wantsToFetch": "Roo ingin mengambil instruksi detail untuk membantu tugas saat ini" + "wantsToFetch": "Zoo ingin mengambil instruksi detail untuk membantu tugas saat ini" }, "fileOperations": { "wantsToRead": "Zoo ingin membaca file ini", @@ -248,8 +248,8 @@ "wantsToGenerateImageProtected": "Zoo ingin menghasilkan gambar di lokasi yang dilindungi", "didGenerateImage": "Zoo telah menghasilkan gambar", "wantsToCreate": "Zoo ingin membuat file baru", - "wantsToSearchReplace": "Roo ingin mencari dan mengganti di file ini", - "didSearchReplace": "Roo melakukan pencarian dan penggantian pada file ini", + "wantsToSearchReplace": "Zoo ingin mencari dan mengganti di file ini", + "didSearchReplace": "Zoo melakukan pencarian dan penggantian pada file ini", "wantsToInsert": "Zoo ingin menyisipkan konten ke file ini", "wantsToInsertWithLineNumber": "Zoo ingin menyisipkan konten ke file ini di baris {{lineNumber}}", "wantsToInsertAtEnd": "Zoo ingin menambahkan konten ke akhir file ini" diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index f98af2bc28..b2943e63bc 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Anda dapat mengonfigurasi pintasan global untuk pengaturan ini di preferensi IDE Anda.", - "description": "Izinkan Roo untuk secara otomatis melakukan operasi tanpa memerlukan persetujuan. Aktifkan pengaturan ini hanya jika kamu sepenuhnya mempercayai AI dan memahami risiko keamanan yang terkait.", + "description": "Izinkan Zoo untuk secara otomatis melakukan operasi tanpa memerlukan persetujuan. Aktifkan pengaturan ini hanya jika kamu sepenuhnya mempercayai AI dan memahami risiko keamanan yang terkait.", "enabled": "Auto-Approve Diaktifkan", "toggleAriaLabel": "Beralih persetujuan otomatis", "disabledAriaLabel": "Persetujuan otomatis dinonaktifkan - pilih opsi terlebih dahulu", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index d6917d1be0..7085edee5b 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -141,7 +141,7 @@ "forNextMode": "per la prossima modalità", "forPreviousMode": "per la modalità precedente", "instructions": { - "wantsToFetch": "Roo vuole recuperare istruzioni dettagliate per aiutare con l'attività corrente" + "wantsToFetch": "Zoo vuole recuperare istruzioni dettagliate per aiutare con l'attività corrente" }, "error": "Errore", "diffError": { @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo vuole modificare questo file al di fuori dell'area di lavoro", "wantsToEditProtected": "Zoo vuole modificare un file di configurazione protetto", "wantsToCreate": "Zoo vuole creare un nuovo file", - "wantsToSearchReplace": "Roo vuole eseguire ricerca e sostituzione in questo file", - "didSearchReplace": "Roo ha eseguito ricerca e sostituzione in questo file", + "wantsToSearchReplace": "Zoo vuole eseguire ricerca e sostituzione in questo file", + "didSearchReplace": "Zoo ha eseguito ricerca e sostituzione in questo file", "wantsToInsert": "Zoo vuole inserire contenuto in questo file", "wantsToInsertWithLineNumber": "Zoo vuole inserire contenuto in questo file alla riga {{lineNumber}}", "wantsToInsertAtEnd": "Zoo vuole aggiungere contenuto alla fine di questo file", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index ea66a2ad23..6bd638c796 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Puoi configurare una scorciatoia globale per questa impostazione nelle preferenze del tuo IDE.", - "description": "Permetti a Roo di eseguire automaticamente operazioni senza richiedere approvazione. Abilita queste impostazioni solo se ti fidi completamente dell'IA e comprendi i rischi di sicurezza associati.", + "description": "Permetti a Zoo di eseguire automaticamente operazioni senza richiedere approvazione. Abilita queste impostazioni solo se ti fidi completamente dell'IA e comprendi i rischi di sicurezza associati.", "enabled": "Auto-approvazione abilitata", "toggleAriaLabel": "Attiva/disattiva approvazione automatica", "disabledAriaLabel": "Approvazione automatica disabilitata - seleziona prima le opzioni", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 17665a3754..8abbe5792c 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -130,7 +130,7 @@ "title": "モード", "marketplace": "モードマーケットプレイス", "settings": "モード設定", - "description": "Rooの動作をカスタマイズする専門的なペルソナ。", + "description": "Zooの動作をカスタマイズする専門的なペルソナ。", "searchPlaceholder": "モードを検索...", "noResults": "結果が見つかりません" }, @@ -144,7 +144,7 @@ "diffError": { "title": "編集に失敗しました" }, - "troubleMessage": "Rooに問題が発生しています...", + "troubleMessage": "Zooに問題が発生しています...", "apiRequest": { "title": "APIリクエスト", "failed": "APIリクエスト失敗", @@ -187,42 +187,42 @@ "current": "現在" }, "instructions": { - "wantsToFetch": "Rooは現在のタスクを支援するための詳細な指示を取得したい" + "wantsToFetch": "Zooは現在のタスクを支援するための詳細な指示を取得したい" }, "fileOperations": { - "wantsToRead": "Rooはこのファイルを読みたい", - "wantsToReadOutsideWorkspace": "Rooはワークスペース外のこのファイルを読みたい", - "didRead": "Rooはこのファイルを読みました", - "wantsToEdit": "Rooはこのファイルを編集したい", - "wantsToEditOutsideWorkspace": "Rooはワークスペース外のこのファイルを編集したい", - "wantsToEditProtected": "Rooは保護された設定ファイルを編集したい", - "wantsToCreate": "Rooは新しいファイルを作成したい", - "wantsToSearchReplace": "Rooはこのファイルで検索と置換を行う", - "didSearchReplace": "Rooはこのファイルで検索と置換を実行しました", - "wantsToInsert": "Rooはこのファイルにコンテンツを挿入したい", - "wantsToInsertWithLineNumber": "Rooはこのファイルの{{lineNumber}}行目にコンテンツを挿入したい", - "wantsToInsertAtEnd": "Rooはこのファイルの末尾にコンテンツを追加したい", + "wantsToRead": "Zooはこのファイルを読みたい", + "wantsToReadOutsideWorkspace": "Zooはワークスペース外のこのファイルを読みたい", + "didRead": "Zooはこのファイルを読みました", + "wantsToEdit": "Zooはこのファイルを編集したい", + "wantsToEditOutsideWorkspace": "Zooはワークスペース外のこのファイルを編集したい", + "wantsToEditProtected": "Zooは保護された設定ファイルを編集したい", + "wantsToCreate": "Zooは新しいファイルを作成したい", + "wantsToSearchReplace": "Zooはこのファイルで検索と置換を行う", + "didSearchReplace": "Zooはこのファイルで検索と置換を実行しました", + "wantsToInsert": "Zooはこのファイルにコンテンツを挿入したい", + "wantsToInsertWithLineNumber": "Zooはこのファイルの{{lineNumber}}行目にコンテンツを挿入したい", + "wantsToInsertAtEnd": "Zooはこのファイルの末尾にコンテンツを追加したい", "wantsToReadAndXMore": "Zoo はこのファイルと他に {{count}} 個のファイルを読み込もうとしています", - "wantsToReadMultiple": "Rooは複数のファイルを読み取ろうとしています", - "wantsToApplyBatchChanges": "Rooは複数のファイルに変更を適用したい", - "wantsToGenerateImage": "Rooは画像を生成したい", - "wantsToGenerateImageOutsideWorkspace": "Rooはワークスペース外で画像を生成したい", - "wantsToGenerateImageProtected": "Rooは保護された場所で画像を生成したい", - "didGenerateImage": "Rooは画像を生成しました" + "wantsToReadMultiple": "Zooは複数のファイルを読み取ろうとしています", + "wantsToApplyBatchChanges": "Zooは複数のファイルに変更を適用したい", + "wantsToGenerateImage": "Zooは画像を生成したい", + "wantsToGenerateImageOutsideWorkspace": "Zooはワークスペース外で画像を生成したい", + "wantsToGenerateImageProtected": "Zooは保護された場所で画像を生成したい", + "didGenerateImage": "Zooは画像を生成しました" }, "directoryOperations": { - "wantsToViewTopLevel": "Rooはこのディレクトリのトップレベルファイルを表示したい", - "didViewTopLevel": "Rooはこのディレクトリのトップレベルファイルを表示しました", - "wantsToViewRecursive": "Rooはこのディレクトリのすべてのファイルを再帰的に表示したい", - "didViewRecursive": "Rooはこのディレクトリのすべてのファイルを再帰的に表示しました", - "wantsToSearch": "Rooはこのディレクトリで {{regex}} を検索したい", - "didSearch": "Rooはこのディレクトリで {{regex}} を検索しました", - "wantsToSearchOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)で {{regex}} を検索したい", - "didSearchOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)で {{regex}} を検索しました", - "wantsToViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示したい", - "didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示しました", - "wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示したい", - "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました", + "wantsToViewTopLevel": "Zooはこのディレクトリのトップレベルファイルを表示したい", + "didViewTopLevel": "Zooはこのディレクトリのトップレベルファイルを表示しました", + "wantsToViewRecursive": "Zooはこのディレクトリのすべてのファイルを再帰的に表示したい", + "didViewRecursive": "Zooはこのディレクトリのすべてのファイルを再帰的に表示しました", + "wantsToSearch": "Zooはこのディレクトリで {{regex}} を検索したい", + "didSearch": "Zooはこのディレクトリで {{regex}} を検索しました", + "wantsToSearchOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)で {{regex}} を検索したい", + "didSearchOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)で {{regex}} を検索しました", + "wantsToViewTopLevelOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示したい", + "didViewTopLevelOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示しました", + "wantsToViewRecursiveOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示したい", + "didViewRecursiveOutsideWorkspace": "Zooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました", "wantsToViewMultipleDirectories": "Zoo は複数のディレクトリを表示したい" }, "commandOutput": "コマンド出力", @@ -246,24 +246,24 @@ "response": "応答", "arguments": "引数", "text": { - "rooSaid": "Rooの発言" + "rooSaid": "Zooの発言" }, "feedback": { "youSaid": "あなたの発言" }, "mcp": { - "wantsToUseTool": "RooはMCPサーバー{{serverName}}でツールを使用したい", - "wantsToAccessResource": "RooはMCPサーバー{{serverName}}のリソースにアクセスしたい" + "wantsToUseTool": "ZooはMCPサーバー{{serverName}}でツールを使用したい", + "wantsToAccessResource": "ZooはMCPサーバー{{serverName}}のリソースにアクセスしたい" }, "modes": { - "wantsToSwitch": "Rooは{{mode}}モードに切り替えたい", - "wantsToSwitchWithReason": "Rooは次の理由で{{mode}}モードに切り替えたい: {{reason}}", - "didSwitch": "Rooは{{mode}}モードに切り替えました", - "didSwitchWithReason": "Rooは次の理由で{{mode}}モードに切り替えました: {{reason}}" + "wantsToSwitch": "Zooは{{mode}}モードに切り替えたい", + "wantsToSwitchWithReason": "Zooは次の理由で{{mode}}モードに切り替えたい: {{reason}}", + "didSwitch": "Zooは{{mode}}モードに切り替えました", + "didSwitchWithReason": "Zooは次の理由で{{mode}}モードに切り替えました: {{reason}}" }, "subtasks": { - "wantsToCreate": "Rooは{{mode}}モードで新しいサブタスクを作成したい", - "wantsToFinish": "Rooはこのサブタスクを終了したい", + "wantsToCreate": "Zooは{{mode}}モードで新しいサブタスクを作成したい", + "wantsToFinish": "Zooはこのサブタスクを終了したい", "newTaskContent": "サブタスク指示", "completionContent": "サブタスク完了", "resultContent": "サブタスク結果", @@ -272,7 +272,7 @@ "goToSubtask": "タスクを表示" }, "questions": { - "hasQuestion": "Rooは質問があります" + "hasQuestion": "Zooは質問があります" }, "taskCompleted": "タスク完了", "modelResponseIncomplete": "モデルの応答が不完全", @@ -288,7 +288,7 @@ "copyToClipboard": "クリップボードにコピー", "copied": "コピーしました!", "diagnostics": "詳細なエラー情報を取得", - "proxyProvider": "プロキシベースのプロバイダーを使用しているようです。ログを確認し、Rooのリクエストを書き換えていないことを確認してください。" + "proxyProvider": "プロキシベースのプロバイダーを使用しているようです。ログを確認し、Zooのリクエストを書き換えていないことを確認してください。" }, "powershell": { "issues": "Windows PowerShellに問題があるようです。こちらを参照してください" @@ -331,8 +331,8 @@ } }, "skill": { - "wantsToLoad": "Rooはスキルを読み込もうとしています", - "didLoad": "Rooはスキルを読み込みました" + "wantsToLoad": "Zooはスキルを読み込もうとしています", + "didLoad": "Zooはスキルを読み込みました" }, "followUpSuggest": { "copyToInput": "入力欄にコピー(またはShift + クリック)", @@ -375,18 +375,18 @@ "ask": { "autoApprovedRequestLimitReached": { "title": "自動承認リクエスト制限に達しました", - "description": "Rooは{{count}}件のAPI自動承認リクエスト制限に達しました。カウントをリセットしてタスクを続行しますか?", + "description": "Zooは{{count}}件のAPI自動承認リクエスト制限に達しました。カウントをリセットしてタスクを続行しますか?", "button": "リセットして続行" }, "autoApprovedCostLimitReached": { "title": "自動承認コスト制限に達しました", - "description": "Rooは自動承認されたコスト制限の${{count}}に達しました。コストをリセットしてタスクを続行しますか?", + "description": "Zooは自動承認されたコスト制限の${{count}}に達しました。コストをリセットしてタスクを続行しますか?", "button": "リセットして続ける" } }, "codebaseSearch": { - "wantsToSearch": "Rooはコードベースで {{query}} を検索したい", - "wantsToSearchWithPath": "Rooは {{path}} 内のコードベースで {{query}} を検索したい", + "wantsToSearch": "Zooはコードベースで {{query}} を検索したい", + "wantsToSearchWithPath": "Zooは {{path}} 内のコードベースで {{query}} を検索したい", "didSearch_one": "1件の結果が見つかりました", "didSearch_other": "{{count}}件の結果が見つかりました", "resultTooltip": "類似度スコア: {{score}} (クリックしてファイルを開く)" @@ -468,8 +468,8 @@ "clickToEdit": "クリックしてメッセージを編集" }, "slashCommand": { - "wantsToRun": "Rooはスラッシュコマンドを実行したい", - "didRun": "Rooはスラッシュコマンドを実行しました" + "wantsToRun": "Zooはスラッシュコマンドを実行したい", + "didRun": "Zooはスラッシュコマンドを実行しました" }, "todo": { "partial": "{{total}}件中{{completed}}件のTo-Doが完了", @@ -488,7 +488,7 @@ "openMcpSettings": "MCP 設定を開く" }, "readCommandOutput": { - "title": "Rooがコマンド出力を読み込みました" + "title": "Zooがコマンド出力を読み込みました" }, "retiredProvider": { "title": "プロバイダーのサポート終了", diff --git a/webview-ui/src/i18n/locales/ja/prompts.json b/webview-ui/src/i18n/locales/ja/prompts.json index 4d7c77e688..eb1b1af251 100644 --- a/webview-ui/src/i18n/locales/ja/prompts.json +++ b/webview-ui/src/i18n/locales/ja/prompts.json @@ -9,7 +9,7 @@ "editModesConfig": "モード設定を編集", "editGlobalModes": "グローバルモードを編集", "editProjectModes": "プロジェクトモードを編集 (.roomodes)", - "createModeHelpText": "モードはRooの振る舞いを調整する専門的なペルソナです。<0>モードの使用について学ぶか、<1>モードのカスタマイズについて学ぶ。", + "createModeHelpText": "モードはZooの振る舞いを調整する専門的なペルソナです。<0>モードの使用について学ぶか、<1>モードのカスタマイズについて学ぶ。", "selectMode": "モードを検索" }, "apiConfiguration": { @@ -33,7 +33,7 @@ "roleDefinition": { "title": "役割の定義", "resetToDefault": "デフォルトにリセット", - "description": "このモードのRooの専門知識と個性を定義します。この説明は、Rooが自身をどのように表現し、タスクにどのように取り組むかを形作ります。" + "description": "このモードのZooの専門知識と個性を定義します。この説明は、Zooが自身をどのように表現し、タスクにどのように取り組むかを形作ります。" }, "description": { "title": "短い説明(人間向け)", @@ -102,7 +102,7 @@ "types": { "ENHANCE": { "label": "プロンプトを強化", - "description": "プロンプト強化を使用して、入力に合わせたカスタマイズされた提案や改善を得ることができます。これにより、Rooがあなたの意図を理解し、最適な回答を提供できます。チャットの✨アイコンから利用できます。" + "description": "プロンプト強化を使用して、入力に合わせたカスタマイズされた提案や改善を得ることができます。これにより、Zooがあなたの意図を理解し、最適な回答を提供できます。チャットの✨アイコンから利用できます。" }, "CONDENSE": { "label": "コンテキスト圧縮", @@ -167,7 +167,7 @@ }, "roleDefinition": { "label": "役割の定義", - "description": "このモードのRooの専門知識と個性を定義します。" + "description": "このモードのZooの専門知識と個性を定義します。" }, "whenToUse": { "label": "使用タイミング(オプション)", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index eed8315620..96fba3521a 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -165,7 +165,7 @@ "footer": "ルールは一般ガイダンスの前にモード固有ガイダンスを読み込みます。グローバルルールはワークスペースルールより先に読み込まれます。" }, "prompts": { - "description": "プロンプトの強化、コードの説明、問題の修正などの迅速なアクションに使用されるサポートプロンプトを設定します。これらのプロンプトは、Rooが一般的な開発タスクでより良いサポートを提供するのに役立ちます。" + "description": "プロンプトの強化、コードの説明、問題の修正などの迅速なアクションに使用されるサポートプロンプトを設定します。これらのプロンプトは、Zooが一般的な開発タスクでより良いサポートを提供するのに役立ちます。" }, "codeIndex": { "title": "コードベースのインデックス作成", @@ -283,16 +283,16 @@ }, "autoApprove": { "toggleShortcut": "IDEの環境設定で、この設定のグローバルショートカットを設定できます。", - "description": "Rooが承認なしで自動的に操作を実行できるようにします。AIを完全に信頼し、関連するセキュリティリスクを理解している場合にのみ、これらの設定を有効にしてください。", + "description": "Zooが承認なしで自動的に操作を実行できるようにします。AIを完全に信頼し、関連するセキュリティリスクを理解している場合にのみ、これらの設定を有効にしてください。", "enabled": "自動承認が有効", "toggleAriaLabel": "自動承認の切り替え", "disabledAriaLabel": "自動承認が無効です - 最初にオプションを選択してください", "readOnly": { "label": "読み取り", - "description": "有効にすると、Rooは承認ボタンをクリックすることなく、自動的にディレクトリの内容を表示してファイルを読み取ります。", + "description": "有効にすると、Zooは承認ボタンをクリックすることなく、自動的にディレクトリの内容を表示してファイルを読み取ります。", "outsideWorkspace": { "label": "ワークスペース外のファイルを含める", - "description": "Rooが承認なしで現在のワークスペース外のファイルを読み取ることを許可します。" + "description": "Zooが承認なしで現在のワークスペース外のファイルを読み取ることを許可します。" } }, "write": { @@ -301,11 +301,11 @@ "delayLabel": "診断が潜在的な問題を検出できるよう、書き込み後に遅延を設ける", "outsideWorkspace": { "label": "ワークスペース外のファイルを含める", - "description": "Rooが承認なしで現在のワークスペース外のファイルを作成・編集することを許可します。" + "description": "Zooが承認なしで現在のワークスペース外のファイルを作成・編集することを許可します。" }, "protected": { "label": "保護されたファイルを含める", - "description": "Rooが保護されたファイル(.rooignoreや.roo/設定ファイルなど)を承認なしで作成・編集することを許可します。" + "description": "Zooが保護されたファイル(.rooignoreや.roo/設定ファイルなど)を承認なしで作成・編集することを許可します。" } }, "mcp": { @@ -616,7 +616,7 @@ }, "consecutiveMistakeLimit": { "label": "エラーと繰り返しの制限", - "description": "「Rooが問題を抱えています」ダイアログを表示するまでの連続エラーまたは繰り返しアクションの数。この安全機構を無効にするには0に設定します(トリガーされません)。", + "description": "「Zooが問題を抱えています」ダイアログを表示するまでの連続エラーまたは繰り返しアクションの数。この安全機構を無効にするには0に設定します(トリガーされません)。", "unlimitedDescription": "無制限のリトライが有効です(自動進行)。ダイアログは表示されません。", "warning": "⚠️ 0に設定すると無制限のリトライが可能になり、API使用量が大幅に増加する可能性があります" }, @@ -660,18 +660,18 @@ }, "enable": { "label": "自動チェックポイントを有効化", - "description": "有効にすると、Rooはタスク実行中に自動的にチェックポイントを作成し、変更の確認や以前の状態への復帰を容易にします。 <0>詳細情報" + "description": "有効にすると、Zooはタスク実行中に自動的にチェックポイントを作成し、変更の確認や以前の状態への復帰を容易にします。 <0>詳細情報" } }, "notifications": { "sound": { "label": "サウンドエフェクトを有効化", - "description": "有効にすると、Rooは通知やイベントのためにサウンドエフェクトを再生します。", + "description": "有効にすると、Zooは通知やイベントのためにサウンドエフェクトを再生します。", "volumeLabel": "音量" }, "tts": { "label": "音声合成を有効化", - "description": "有効にすると、Rooは音声合成を使用して応答を音声で読み上げます。", + "description": "有効にすると、Zooは音声合成を使用して応答を音声で読み上げます。", "speedLabel": "速度" } }, @@ -679,7 +679,7 @@ "description": "AIのコンテキストウィンドウに含まれる情報を制御し、token使用量とレスポンスの品質に影響します", "autoCondenseContextPercent": { "label": "インテリジェントなコンテキスト圧縮をトリガーするしきい値", - "description": "コンテキストウィンドウがこのしきい値に達すると、Rooは自動的に圧縮します。" + "description": "コンテキストウィンドウがこのしきい値に達すると、Zooは自動的に圧縮します。" }, "condensingApiConfiguration": { "label": "コンテキスト圧縮用のAPI設定", @@ -695,7 +695,7 @@ }, "autoCondenseContext": { "name": "インテリジェントなコンテキスト圧縮を自動的にトリガーする", - "description": "有効にすると、Rooは閾値に達したときに自動的にコンテキストを圧縮します。無効にすると、手動でコンテキスト圧縮をトリガーできます。" + "description": "有効にすると、Zooは閾値に達したときに自動的にコンテキストを圧縮します。無効にすると、手動でコンテキスト圧縮をトリガーできます。" }, "openTabs": { "label": "オープンタブコンテキスト制限", @@ -711,7 +711,7 @@ }, "maxReadFile": { "label": "ファイル読み込み自動切り詰めしきい値", - "description": "モデルが開始/終了の値を指定しない場合、Rooはこの行数を読み込みます。この数がファイルの総行数より少ない場合、Rooはコード定義の行番号インデックスを生成します。特殊なケース:-1はRooにファイル全体を読み込むよう指示し(インデックス作成なし)、0は行を読み込まず最小限のコンテキストのために行インデックスのみを提供するよう指示します。低い値は初期コンテキスト使用量を最小限に抑え、後続の正確な行範囲の読み込みを可能にします。明示的な開始/終了の要求はこの設定による制限を受けません。", + "description": "モデルが開始/終了の値を指定しない場合、Zooはこの行数を読み込みます。この数がファイルの総行数より少ない場合、Zooはコード定義の行番号インデックスを生成します。特殊なケース:-1はZooにファイル全体を読み込むよう指示し(インデックス作成なし)、0は行を読み込まず最小限のコンテキストのために行インデックスのみを提供するよう指示します。低い値は初期コンテキスト使用量を最小限に抑え、後続の正確な行範囲の読み込みを可能にします。明示的な開始/終了の要求はこの設定による制限を受けません。", "lines": "行", "always_full_read": "常にファイル全体を読み込む" }, @@ -788,15 +788,15 @@ }, "outputLineLimit": { "label": "ターミナル出力制限", - "description": "制限内に収めるため最初と最後の行を保持し、中間を削除します。トークンを節約するには下げる;Rooに中間の詳細を与えるには上げる。Rooはコンテンツがスキップされた箇所にプレースホルダーを表示します。<0>詳細情報" + "description": "制限内に収めるため最初と最後の行を保持し、中間を削除します。トークンを節約するには下げる;Zooに中間の詳細を与えるには上げる。Zooはコンテンツがスキップされた箇所にプレースホルダーを表示します。<0>詳細情報" }, "outputCharacterLimit": { "label": "ターミナル文字制限", - "description": "出力サイズにハードキャップを適用してメモリ問題を防ぐため、行制限を上書きします。超過した場合、最初と最後を保持し、コンテンツがスキップされた箇所にRooにプレースホルダーを表示します。<0>詳細情報" + "description": "出力サイズにハードキャップを適用してメモリ問題を防ぐため、行制限を上書きします。超過した場合、最初と最後を保持し、コンテンツがスキップされた箇所にZooにプレースホルダーを表示します。<0>詳細情報" }, "outputPreviewSize": { "label": "コマンド出力プレビューサイズ", - "description": "Rooが直接確認できるコマンド出力の量を制御します。完全な出力は常に保存され、必要に応じてアクセス可能です。", + "description": "Zooが直接確認できるコマンド出力の量を制御します。完全な出力は常に保存され、必要に応じてアクセス可能です。", "options": { "small": "小 (5KB)", "medium": "中 (10KB)", @@ -853,7 +853,7 @@ "advanced": { "diff": { "label": "diff経由の編集を有効化", - "description": "有効にすると、Rooはファイルをより迅速に編集でき、切り詰められた全ファイル書き込みを自動的に拒否します。", + "description": "有効にすると、Zooはファイルをより迅速に編集でき、切り詰められた全ファイル書き込みを自動的に拒否します。", "strategy": { "label": "Diff戦略", "options": { @@ -870,7 +870,7 @@ }, "todoList": { "label": "ToDoリストツールを有効にする", - "description": "有効にすると、Rooはタスクの進捗を追跡するためのToDoリストを作成・管理できます。これにより、複雑なタスクを管理しやすいステップに整理できます。" + "description": "有効にすると、Zooはタスクの進捗を追跡するためのToDoリストを作成・管理できます。これにより、複雑なタスクを管理しやすいステップに整理できます。" } }, "experimental": { @@ -880,15 +880,15 @@ }, "INSERT_BLOCK": { "name": "実験的なコンテンツ挿入ツールを使用する", - "description": "実験的なコンテンツ挿入ツールを有効にし、Rooがdiffを作成せずに特定の行番号にコンテンツを挿入できるようにします。" + "description": "実験的なコンテンツ挿入ツールを有効にし、Zooがdiffを作成せずに特定の行番号にコンテンツを挿入できるようにします。" }, "MULTI_SEARCH_AND_REPLACE": { "name": "実験的なマルチブロックdiffツールを使用する", - "description": "有効にすると、Rooはマルチブロックdiffツールを使用します。これにより、1つのリクエストでファイル内の複数のコードブロックを更新しようとします。" + "description": "有効にすると、Zooはマルチブロックdiffツールを使用します。これにより、1つのリクエストでファイル内の複数のコードブロックを更新しようとします。" }, "CONCURRENT_FILE_READS": { "name": "並行ファイル読み取りを有効にする", - "description": "有効にすると、Rooは1回のリクエストで複数のファイル を読み取ることができます。無効にすると、Rooはファイルを1つずつ読み取る必要があります。能力の低いモデルで作業する場合や、ファイルアクセスをより細かく制御したい場合は、無効にすると役立ちます。" + "description": "有効にすると、Zooは1回のリクエストで複数のファイル を読み取ることができます。無効にすると、Zooはファイルを1つずつ読み取る必要があります。能力の低いモデルで作業する場合や、ファイルアクセスをより細かく制御したい場合は、無効にすると役立ちます。" }, "MARKETPLACE": { "name": "Marketplaceを有効にする", @@ -896,7 +896,7 @@ }, "PREVENT_FOCUS_DISRUPTION": { "name": "バックグラウンド編集", - "description": "有効にすると、エディターのフォーカス中断を防ぎます。ファイル編集は差分ビューを開いたりフォーカスを奪ったりすることなく、バックグラウンドで行われます。Rooが変更を行っている間も中断されることなく作業を続けることができます。ファイルは診断をキャプチャするためにフォーカスなしで開くか、完全に閉じたままにできます。" + "description": "有効にすると、エディターのフォーカス中断を防ぎます。ファイル編集は差分ビューを開いたりフォーカスを奪ったりすることなく、バックグラウンドで行われます。Zooが変更を行っている間も中断されることなく作業を続けることができます。ファイルは診断をキャプチャするためにフォーカスなしで開くか、完全に閉じたままにできます。" }, "ASSISTANT_MESSAGE_PARSER": { "name": "新しいメッセージパーサーを使う", @@ -910,7 +910,7 @@ "providerLabel": "プロバイダー", "providerDescription": "画像生成に使用するプロバイダーを選択", "name": "AI画像生成を有効にする", - "description": "有効にすると、RooはOpenRouterの画像生成モデルを使用してテキストプロンプトから画像を生成できます。OpenRouter APIキーの設定が必要です。", + "description": "有効にすると、ZooはOpenRouterの画像生成モデルを使用してテキストプロンプトから画像を生成できます。OpenRouter APIキーの設定が必要です。", "openRouterApiKeyLabel": "OpenRouter APIキー", "openRouterApiKeyPlaceholder": "OpenRouter APIキーを入力してください", "getApiKeyText": "APIキーを取得する場所", @@ -921,11 +921,11 @@ }, "RUN_SLASH_COMMAND": { "name": "モデル開始スラッシュコマンドを有効にする", - "description": "有効にすると、Rooがワークフローを実行するためにあなたのスラッシュコマンドを実行できます。" + "description": "有効にすると、Zooがワークフローを実行するためにあなたのスラッシュコマンドを実行できます。" }, "CUSTOM_TOOLS": { "name": "カスタムツールを有効化", - "description": "有効にすると、Rooはプロジェクトの.roo/toolsディレクトリまたはグローバルツール用の~/.roo/toolsからカスタムTypeScript/JavaScriptツールを読み込んで使用できます。注意:これらのツールは自動的に承認されます。", + "description": "有効にすると、Zooはプロジェクトの.roo/toolsディレクトリまたはグローバルツール用の~/.roo/toolsからカスタムTypeScript/JavaScriptツールを読み込んで使用できます。注意:これらのツールは自動的に承認されます。", "toolsHeader": "利用可能なカスタムツール", "noTools": "カスタムツールが読み込まれていません。プロジェクトの.roo/toolsディレクトリまたはグローバルツール用の~/.roo/toolsに.tsまたは.jsファイルを追加してください。", "refreshButton": "更新", @@ -937,7 +937,7 @@ }, "promptCaching": { "label": "プロンプトキャッシュを無効化", - "description": "チェックすると、Rooはこのモデルに対してプロンプトキャッシュを使用しません。" + "description": "チェックすると、Zooはこのモデルに対してプロンプトキャッシュを使用しません。" }, "temperature": { "useCustom": "カスタム温度を使用", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 53186e7afe..ac7b6c4c05 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -130,7 +130,7 @@ "title": "모드", "marketplace": "모드 마켓플레이스", "settings": "모드 설정", - "description": "Roo의 행동을 맞춤화하는 전문화된 페르소나.", + "description": "Zoo의 행동을 맞춤화하는 전문화된 페르소나.", "searchPlaceholder": "모드 검색...", "noResults": "결과를 찾을 수 없습니다" }, @@ -144,7 +144,7 @@ "diffError": { "title": "편집 실패" }, - "troubleMessage": "Roo에 문제가 발생했습니다...", + "troubleMessage": "Zoo에 문제가 발생했습니다...", "apiRequest": { "title": "API 요청", "failed": "API 요청 실패", @@ -187,43 +187,43 @@ "current": "현재" }, "instructions": { - "wantsToFetch": "Roo는 현재 작업을 지원하기 위해 자세한 지침을 가져오려고 합니다" + "wantsToFetch": "Zoo는 현재 작업을 지원하기 위해 자세한 지침을 가져오려고 합니다" }, "fileOperations": { - "wantsToRead": "Roo가 이 파일을 읽고 싶어합니다", - "wantsToReadOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 읽고 싶어합니다", - "didRead": "Roo가 이 파일을 읽었습니다", - "wantsToEdit": "Roo가 이 파일을 편집하고 싶어합니다", - "wantsToEditOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 편집하고 싶어합니다", - "wantsToEditProtected": "Roo가 보호된 설정 파일을 편집하고 싶어합니다", - "wantsToCreate": "Roo가 새 파일을 만들고 싶어합니다", - "wantsToSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행하고 싶어합니다", - "didSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행했습니다", - "wantsToInsert": "Roo가 이 파일에 내용을 삽입하고 싶어합니다", - "wantsToInsertWithLineNumber": "Roo가 이 파일의 {{lineNumber}}번 줄에 내용을 삽입하고 싶어합니다", - "wantsToInsertAtEnd": "Roo가 이 파일의 끝에 내용을 추가하고 싶어합니다", - "wantsToReadAndXMore": "Roo가 이 파일과 {{count}}개의 파일을 더 읽으려고 합니다", - "wantsToReadMultiple": "Roo가 여러 파일을 읽으려고 합니다", - "wantsToApplyBatchChanges": "Roo가 여러 파일에 변경 사항을 적용하고 싶어합니다", - "wantsToGenerateImage": "Roo가 이미지를 생성하고 싶어합니다", - "wantsToGenerateImageOutsideWorkspace": "Roo가 워크스페이스 외부에서 이미지를 생성하고 싶어합니다", - "wantsToGenerateImageProtected": "Roo가 보호된 위치에서 이미지를 생성하고 싶어합니다", - "didGenerateImage": "Roo가 이미지를 생성했습니다" + "wantsToRead": "Zoo가 이 파일을 읽고 싶어합니다", + "wantsToReadOutsideWorkspace": "Zoo가 워크스페이스 외부의 이 파일을 읽고 싶어합니다", + "didRead": "Zoo가 이 파일을 읽었습니다", + "wantsToEdit": "Zoo가 이 파일을 편집하고 싶어합니다", + "wantsToEditOutsideWorkspace": "Zoo가 워크스페이스 외부의 이 파일을 편집하고 싶어합니다", + "wantsToEditProtected": "Zoo가 보호된 설정 파일을 편집하고 싶어합니다", + "wantsToCreate": "Zoo가 새 파일을 만들고 싶어합니다", + "wantsToSearchReplace": "Zoo가 이 파일에서 검색 및 바꾸기를 수행하고 싶어합니다", + "didSearchReplace": "Zoo가 이 파일에서 검색 및 바꾸기를 수행했습니다", + "wantsToInsert": "Zoo가 이 파일에 내용을 삽입하고 싶어합니다", + "wantsToInsertWithLineNumber": "Zoo가 이 파일의 {{lineNumber}}번 줄에 내용을 삽입하고 싶어합니다", + "wantsToInsertAtEnd": "Zoo가 이 파일의 끝에 내용을 추가하고 싶어합니다", + "wantsToReadAndXMore": "Zoo가 이 파일과 {{count}}개의 파일을 더 읽으려고 합니다", + "wantsToReadMultiple": "Zoo가 여러 파일을 읽으려고 합니다", + "wantsToApplyBatchChanges": "Zoo가 여러 파일에 변경 사항을 적용하고 싶어합니다", + "wantsToGenerateImage": "Zoo가 이미지를 생성하고 싶어합니다", + "wantsToGenerateImageOutsideWorkspace": "Zoo가 워크스페이스 외부에서 이미지를 생성하고 싶어합니다", + "wantsToGenerateImageProtected": "Zoo가 보호된 위치에서 이미지를 생성하고 싶어합니다", + "didGenerateImage": "Zoo가 이미지를 생성했습니다" }, "directoryOperations": { - "wantsToViewTopLevel": "Roo가 이 디렉토리의 최상위 파일을 보고 싶어합니다", - "didViewTopLevel": "Roo가 이 디렉토리의 최상위 파일을 보았습니다", - "wantsToViewRecursive": "Roo가 이 디렉토리의 모든 파일을 재귀적으로 보고 싶어합니다", - "didViewRecursive": "Roo가 이 디렉토리의 모든 파일을 재귀적으로 보았습니다", - "wantsToSearch": "Roo가 이 디렉토리에서 {{regex}}을(를) 검색하고 싶어합니다", - "didSearch": "Roo가 이 디렉토리에서 {{regex}}을(를) 검색했습니다", - "wantsToSearchOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 {{regex}}을(를) 검색하고 싶어합니다", - "didSearchOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 {{regex}}을(를) 검색했습니다", - "wantsToViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보고 싶어합니다", - "didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다", - "wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다", - "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다", - "wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다" + "wantsToViewTopLevel": "Zoo가 이 디렉토리의 최상위 파일을 보고 싶어합니다", + "didViewTopLevel": "Zoo가 이 디렉토리의 최상위 파일을 보았습니다", + "wantsToViewRecursive": "Zoo가 이 디렉토리의 모든 파일을 재귀적으로 보고 싶어합니다", + "didViewRecursive": "Zoo가 이 디렉토리의 모든 파일을 재귀적으로 보았습니다", + "wantsToSearch": "Zoo가 이 디렉토리에서 {{regex}}을(를) 검색하고 싶어합니다", + "didSearch": "Zoo가 이 디렉토리에서 {{regex}}을(를) 검색했습니다", + "wantsToSearchOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)에서 {{regex}}을(를) 검색하고 싶어합니다", + "didSearchOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)에서 {{regex}}을(를) 검색했습니다", + "wantsToViewTopLevelOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보고 싶어합니다", + "didViewTopLevelOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다", + "wantsToViewRecursiveOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다", + "didViewRecursiveOutsideWorkspace": "Zoo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다", + "wantsToViewMultipleDirectories": "Zoo가 여러 디렉토리를 보려고 합니다" }, "commandOutput": "명령 출력", "commandExecution": { @@ -252,18 +252,18 @@ "youSaid": "당신은 말했다" }, "mcp": { - "wantsToUseTool": "Roo가 {{serverName}} MCP 서버에서 도구를 사용하고 싶어합니다", - "wantsToAccessResource": "Roo가 {{serverName}} MCP 서버에서 리소스에 접근하고 싶어합니다" + "wantsToUseTool": "Zoo가 {{serverName}} MCP 서버에서 도구를 사용하고 싶어합니다", + "wantsToAccessResource": "Zoo가 {{serverName}} MCP 서버에서 리소스에 접근하고 싶어합니다" }, "modes": { - "wantsToSwitch": "Roo가 {{mode}} 모드로 전환하고 싶어합니다", - "wantsToSwitchWithReason": "Roo가 다음 이유로 {{mode}} 모드로 전환하고 싶어합니다: {{reason}}", - "didSwitch": "Roo가 {{mode}} 모드로 전환했습니다", - "didSwitchWithReason": "Roo가 다음 이유로 {{mode}} 모드로 전환했습니다: {{reason}}" + "wantsToSwitch": "Zoo가 {{mode}} 모드로 전환하고 싶어합니다", + "wantsToSwitchWithReason": "Zoo가 다음 이유로 {{mode}} 모드로 전환하고 싶어합니다: {{reason}}", + "didSwitch": "Zoo가 {{mode}} 모드로 전환했습니다", + "didSwitchWithReason": "Zoo가 다음 이유로 {{mode}} 모드로 전환했습니다: {{reason}}" }, "subtasks": { - "wantsToCreate": "Roo가 {{mode}} 모드에서 새 하위 작업을 만들고 싶어합니다", - "wantsToFinish": "Roo가 이 하위 작업을 완료하고 싶어합니다", + "wantsToCreate": "Zoo가 {{mode}} 모드에서 새 하위 작업을 만들고 싶어합니다", + "wantsToFinish": "Zoo가 이 하위 작업을 완료하고 싶어합니다", "newTaskContent": "하위 작업 지침", "completionContent": "하위 작업 완료", "resultContent": "하위 작업 결과", @@ -272,7 +272,7 @@ "goToSubtask": "작업 보기" }, "questions": { - "hasQuestion": "Roo에게 질문이 있습니다" + "hasQuestion": "Zoo에게 질문이 있습니다" }, "taskCompleted": "작업 완료", "modelResponseIncomplete": "모델 응답 불완전", @@ -288,7 +288,7 @@ "copyToClipboard": "클립보드에 복사", "copied": "복사됨!", "diagnostics": "상세한 오류 정보 가져오기", - "proxyProvider": "프록시 기반 제공자를 사용하는 것 같습니다. 로그를 확인하고 Roo의 요청을 다시 작성하지 않는지 확인하세요." + "proxyProvider": "프록시 기반 제공자를 사용하는 것 같습니다. 로그를 확인하고 Zoo의 요청을 다시 작성하지 않는지 확인하세요." }, "powershell": { "issues": "Windows PowerShell에 문제가 있는 것 같습니다. 다음을 참조하세요" @@ -331,8 +331,8 @@ } }, "skill": { - "wantsToLoad": "Roo가 스킬을 로드하려고 합니다", - "didLoad": "Roo가 스킬을 로드했습니다" + "wantsToLoad": "Zoo가 스킬을 로드하려고 합니다", + "didLoad": "Zoo가 스킬을 로드했습니다" }, "followUpSuggest": { "copyToInput": "입력창에 복사 (또는 Shift + 클릭)", @@ -375,18 +375,18 @@ "ask": { "autoApprovedRequestLimitReached": { "title": "자동 승인 요청 한도 도달", - "description": "Roo가 {{count}}개의 API 요청(들)에 대한 자동 승인 한도에 도달했습니다. 카운트를 재설정하고 작업을 계속하시겠습니까?", + "description": "Zoo가 {{count}}개의 API 요청(들)에 대한 자동 승인 한도에 도달했습니다. 카운트를 재설정하고 작업을 계속하시겠습니까?", "button": "재설정 후 계속" }, "autoApprovedCostLimitReached": { - "description": "Roo가 자동 승인된 비용 한도인 ${{count}}에 도달했습니다. 비용을 초기화하고 작업을 계속하시겠습니까?", + "description": "Zoo가 자동 승인된 비용 한도인 ${{count}}에 도달했습니다. 비용을 초기화하고 작업을 계속하시겠습니까?", "title": "자동 승인 비용 한도에 도달함", "button": "재설정 후 계속하기" } }, "codebaseSearch": { - "wantsToSearch": "Roo가 코드베이스에서 {{query}}을(를) 검색하고 싶어합니다", - "wantsToSearchWithPath": "Roo가 {{path}}에서 {{query}}을(를) 검색하고 싶어합니다", + "wantsToSearch": "Zoo가 코드베이스에서 {{query}}을(를) 검색하고 싶어합니다", + "wantsToSearchWithPath": "Zoo가 {{path}}에서 {{query}}을(를) 검색하고 싶어합니다", "didSearch_one": "1개의 결과를 찾았습니다", "didSearch_other": "{{count}}개의 결과를 찾았습니다", "resultTooltip": "유사도 점수: {{score}} (클릭하여 파일 열기)" @@ -468,8 +468,8 @@ "clickToEdit": "클릭하여 메시지 편집" }, "slashCommand": { - "wantsToRun": "Roo가 슬래시 명령어를 실행하려고 합니다", - "didRun": "Roo가 슬래시 명령어를 실행했습니다" + "wantsToRun": "Zoo가 슬래시 명령어를 실행하려고 합니다", + "didRun": "Zoo가 슬래시 명령어를 실행했습니다" }, "todo": { "partial": "{{total}}개의 할 일 중 {{completed}}개 완료", diff --git a/webview-ui/src/i18n/locales/ko/prompts.json b/webview-ui/src/i18n/locales/ko/prompts.json index 204547e4c3..90ac4d0905 100644 --- a/webview-ui/src/i18n/locales/ko/prompts.json +++ b/webview-ui/src/i18n/locales/ko/prompts.json @@ -9,7 +9,7 @@ "editModesConfig": "모드 구성 편집", "editGlobalModes": "전역 모드 편집", "editProjectModes": "프로젝트 모드 편집 (.roomodes)", - "createModeHelpText": "모드는 Roo의 행동을 맞춤화하는 특화된 페르소나입니다. <0>모드 사용에 대해 알아보기 또는 <1>모드 사용자 정의하기.", + "createModeHelpText": "모드는 Zoo의 행동을 맞춤화하는 특화된 페르소나입니다. <0>모드 사용에 대해 알아보기 또는 <1>모드 사용자 정의하기.", "selectMode": "모드 검색" }, "apiConfiguration": { @@ -33,7 +33,7 @@ "roleDefinition": { "title": "역할 정의", "resetToDefault": "기본값으로 재설정", - "description": "이 모드에 대한 Roo의 전문성과 성격을 정의하세요. 이 설명은 Roo가 자신을 어떻게 표현하고 작업에 접근하는지 형성합니다." + "description": "이 모드에 대한 Zoo의 전문성과 성격을 정의하세요. 이 설명은 Zoo가 자신을 어떻게 표현하고 작업에 접근하는지 형성합니다." }, "description": { "title": "짧은 설명 (사람용)", @@ -102,7 +102,7 @@ "types": { "ENHANCE": { "label": "프롬프트 향상", - "description": "입력에 맞춤화된 제안이나 개선을 얻기 위해 프롬프트 향상을 사용하세요. 이를 통해 Roo가 의도를 이해하고 최상의 응답을 제공할 수 있습니다. 채팅에서 ✨ 아이콘을 통해 이용 가능합니다." + "description": "입력에 맞춤화된 제안이나 개선을 얻기 위해 프롬프트 향상을 사용하세요. 이를 통해 Zoo가 의도를 이해하고 최상의 응답을 제공할 수 있습니다. 채팅에서 ✨ 아이콘을 통해 이용 가능합니다." }, "CONDENSE": { "label": "컨텍스트 압축", @@ -167,7 +167,7 @@ }, "roleDefinition": { "label": "역할 정의", - "description": "이 모드에 대한 Roo의 전문성과 성격을 정의하세요." + "description": "이 모드에 대한 Zoo의 전문성과 성격을 정의하세요." }, "whenToUse": { "label": "사용 시기 (선택 사항)", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index ae8fd3b9ef..40313a32f5 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -165,7 +165,7 @@ "footer": "규칙은 일반 지침보다 모드별 지침을 먼저 로드합니다. 전역 규칙은 워크스페이스 규칙보다 먼저 로드됩니다." }, "prompts": { - "description": "프롬프트 향상, 코드 설명, 문제 해결과 같은 빠른 작업에 사용되는 지원 프롬프트를 구성합니다. 이러한 프롬프트는 Roo가 일반적인 개발 작업에 대해 더 나은 지원을 제공하는 데 도움이 됩니다." + "description": "프롬프트 향상, 코드 설명, 문제 해결과 같은 빠른 작업에 사용되는 지원 프롬프트를 구성합니다. 이러한 프롬프트는 Zoo가 일반적인 개발 작업에 대해 더 나은 지원을 제공하는 데 도움이 됩니다." }, "codeIndex": { "title": "코드베이스 인덱싱", @@ -283,16 +283,16 @@ }, "autoApprove": { "toggleShortcut": "IDE 환경 설정에서 이 설정에 대한 전역 바로 가기를 구성할 수 있습니다.", - "description": "Roo가 승인 없이 자동으로 작업을 수행할 수 있도록 허용합니다. AI를 완전히 신뢰하고 관련 보안 위험을 이해하는 경우에만 이러한 설정을 활성화하세요.", + "description": "Zoo가 승인 없이 자동으로 작업을 수행할 수 있도록 허용합니다. AI를 완전히 신뢰하고 관련 보안 위험을 이해하는 경우에만 이러한 설정을 활성화하세요.", "enabled": "자동 승인 활성화됨", "toggleAriaLabel": "자동 승인 전환", "disabledAriaLabel": "자동 승인 비활성화됨 - 먼저 옵션을 선택하세요", "readOnly": { "label": "읽기", - "description": "활성화되면 Roo는 승인 버튼을 클릭하지 않고도 자동으로 디렉토리 내용을 보고 파일을 읽습니다.", + "description": "활성화되면 Zoo는 승인 버튼을 클릭하지 않고도 자동으로 디렉토리 내용을 보고 파일을 읽습니다.", "outsideWorkspace": { "label": "워크스페이스 외부 파일 포함", - "description": "Roo가 승인 없이 현재 워크스페이스 외부의 파일을 읽을 수 있도록 허용합니다." + "description": "Zoo가 승인 없이 현재 워크스페이스 외부의 파일을 읽을 수 있도록 허용합니다." } }, "write": { @@ -301,11 +301,11 @@ "delayLabel": "진단이 잠재적 문제를 감지할 수 있도록 쓰기 후 지연", "outsideWorkspace": { "label": "워크스페이스 외부 파일 포함", - "description": "Roo가 승인 없이 현재 워크스페이스 외부의 파일을 생성하고 편집할 수 있도록 허용합니다." + "description": "Zoo가 승인 없이 현재 워크스페이스 외부의 파일을 생성하고 편집할 수 있도록 허용합니다." }, "protected": { "label": "보호된 파일 포함", - "description": "Roo가 보호된 파일(.rooignore 및 .roo/ 구성 파일 등)을 승인 없이 생성하고 편집할 수 있도록 허용합니다." + "description": "Zoo가 보호된 파일(.rooignore 및 .roo/ 구성 파일 등)을 승인 없이 생성하고 편집할 수 있도록 허용합니다." } }, "mcp": { @@ -616,7 +616,7 @@ }, "consecutiveMistakeLimit": { "label": "오류 및 반복 제한", - "description": "'Roo에 문제가 발생했습니다' 대화 상자를 표시하기 전의 연속 오류 또는 반복 작업 수. 이 안전 메커니즘을 비활성화하려면 0으로 설정하세요(절대 트리거되지 않음).", + "description": "'Zoo에 문제가 발생했습니다' 대화 상자를 표시하기 전의 연속 오류 또는 반복 작업 수. 이 안전 메커니즘을 비활성화하려면 0으로 설정하세요(절대 트리거되지 않음).", "unlimitedDescription": "무제한 재시도 활성화 (자동 진행). 대화 상자가 나타나지 않습니다.", "warning": "⚠️ 0으로 설정하면 무제한 재시도가 허용되어 상당한 API 사용량이 발생할 수 있습니다" }, @@ -660,18 +660,18 @@ }, "enable": { "label": "자동 체크포인트 활성화", - "description": "활성화되면 Roo는 작업 실행 중에 자동으로 체크포인트를 생성하여 변경 사항을 검토하거나 이전 상태로 되돌리기 쉽게 합니다. <0>더 알아보기" + "description": "활성화되면 Zoo는 작업 실행 중에 자동으로 체크포인트를 생성하여 변경 사항을 검토하거나 이전 상태로 되돌리기 쉽게 합니다. <0>더 알아보기" } }, "notifications": { "sound": { "label": "사운드 효과 활성화", - "description": "활성화되면 Roo는 알림 및 이벤트에 대한 사운드 효과를 재생합니다.", + "description": "활성화되면 Zoo는 알림 및 이벤트에 대한 사운드 효과를 재생합니다.", "volumeLabel": "볼륨" }, "tts": { "label": "음성 합성 활성화", - "description": "활성화되면 Roo는 음성 합성을 사용하여 응답을 소리내어 읽습니다.", + "description": "활성화되면 Zoo는 음성 합성을 사용하여 응답을 소리내어 읽습니다.", "speedLabel": "속도" } }, @@ -679,7 +679,7 @@ "description": "AI의 컨텍스트 창에 포함되는 정보를 제어하여 token 사용량과 응답 품질에 영향을 미칩니다", "autoCondenseContextPercent": { "label": "지능적 컨텍스트 압축을 트리거하는 임계값", - "description": "컨텍스트 창이 이 임계값에 도달하면 Roo가 자동으로 압축합니다." + "description": "컨텍스트 창이 이 임계값에 도달하면 Zoo가 자동으로 압축합니다." }, "condensingApiConfiguration": { "label": "컨텍스트 압축을 위한 API 설정", @@ -695,7 +695,7 @@ }, "autoCondenseContext": { "name": "지능적 컨텍스트 압축 자동 트리거", - "description": "활성화되면 Roo는 임계값에 도달했을 때 자동으로 컨텍스트를 압축합니다. 비활성화되면 여전히 수동으로 컨텍스트 압축을 트리거할 수 있습니다." + "description": "활성화되면 Zoo는 임계값에 도달했을 때 자동으로 컨텍스트를 압축합니다. 비활성화되면 여전히 수동으로 컨텍스트 압축을 트리거할 수 있습니다." }, "openTabs": { "label": "열린 탭 컨텍스트 제한", @@ -711,7 +711,7 @@ }, "maxReadFile": { "label": "파일 읽기 자동 축소 임계값", - "description": "모델이 시작/끝 값을 지정하지 않을 때 Roo가 읽는 줄 수입니다. 이 수가 파일의 총 줄 수보다 적으면 Roo는 코드 정의의 줄 번호 인덱스를 생성합니다. 특수한 경우: -1은 Roo에게 전체 파일을 읽도록 지시하고(인덱싱 없이), 0은 줄을 읽지 않고 최소한의 컨텍스트를 위해 줄 인덱스만 제공하도록 지시합니다. 낮은 값은 초기 컨텍스트 사용을 최소화하고, 이후 정확한 줄 범위 읽기를 가능하게 합니다. 명시적 시작/끝 요청은 이 설정의 제한을 받지 않습니다.", + "description": "모델이 시작/끝 값을 지정하지 않을 때 Zoo가 읽는 줄 수입니다. 이 수가 파일의 총 줄 수보다 적으면 Zoo는 코드 정의의 줄 번호 인덱스를 생성합니다. 특수한 경우: -1은 Zoo에게 전체 파일을 읽도록 지시하고(인덱싱 없이), 0은 줄을 읽지 않고 최소한의 컨텍스트를 위해 줄 인덱스만 제공하도록 지시합니다. 낮은 값은 초기 컨텍스트 사용을 최소화하고, 이후 정확한 줄 범위 읽기를 가능하게 합니다. 명시적 시작/끝 요청은 이 설정의 제한을 받지 않습니다.", "lines": "줄", "always_full_read": "항상 전체 파일 읽기" }, @@ -788,15 +788,15 @@ }, "outputLineLimit": { "label": "터미널 출력 제한", - "description": "제한 내로 유지하기 위해 첫 줄과 마지막 줄을 유지하고 중간을 삭제합니다. 토큰을 절약하려면 낮추고; Roo에게 더 많은 중간 세부 정보를 제공하려면 높입니다. Roo는 콘텐츠가 건너뛴 곳에 자리 표시자를 봅니다.<0>자세히 알아보기" + "description": "제한 내로 유지하기 위해 첫 줄과 마지막 줄을 유지하고 중간을 삭제합니다. 토큰을 절약하려면 낮추고; Zoo에게 더 많은 중간 세부 정보를 제공하려면 높입니다. Zoo는 콘텐츠가 건너뛴 곳에 자리 표시자를 봅니다.<0>자세히 알아보기" }, "outputCharacterLimit": { "label": "터미널 문자 제한", - "description": "출력 크기에 대한 엄격한 상한을 적용하여 메모리 문제를 방지하기 위해 줄 제한을 재정의합니다. 초과하면 시작과 끝을 유지하고 내용이 생략된 곳에 Roo에게 자리 표시자를 표시합니다. <0>자세히 알아보기" + "description": "출력 크기에 대한 엄격한 상한을 적용하여 메모리 문제를 방지하기 위해 줄 제한을 재정의합니다. 초과하면 시작과 끝을 유지하고 내용이 생략된 곳에 Zoo에게 자리 표시자를 표시합니다. <0>자세히 알아보기" }, "outputPreviewSize": { "label": "명령 출력 미리보기 크기", - "description": "Roo가 직접 보는 명령 출력량을 제어합니다. 전체 출력은 항상 저장되며 필요할 때 액세스할 수 있습니다.", + "description": "Zoo가 직접 보는 명령 출력량을 제어합니다. 전체 출력은 항상 저장되며 필요할 때 액세스할 수 있습니다.", "options": { "small": "작게 (5KB)", "medium": "보통 (10KB)", @@ -809,7 +809,7 @@ }, "shellIntegrationDisabled": { "label": "인라인 터미널 사용(권장)", - "description": "더 빠르고 안정적인 실행을 위해 셸 프로필/통합��� 우회하려면 인라인 터미널(채팅)에�� 명령을 실행하십시오. 비활성화하면 Roo는 셸 프로필, 프롬프트 및 플러그인과 함께 VS Code 터미널을 사용합니다. <0>자세히 알아보기" + "description": "더 빠르고 안정적인 실행을 위해 셸 프로필/통합을 우회하려면 인라인 터미널(채팅)에서 명령을 실행하십시오. 비활성화하면 Zoo는 셸 프로필, 프롬프트 및 플러그인과 함께 VS Code 터미널을 사용합니다. <0>자세히 알아보기" }, "commandDelay": { "label": "터미널 명령 지연", @@ -837,7 +837,7 @@ }, "inheritEnv": { "label": "환경 변수 상속", - "description": "부모 VS Code 프로세���에서 환경 변수를 상속하려면 이 기능을 켜십시오. <0>자세히 알아보기" + "description": "부모 VS Code 프로세스에서 환경 변수를 상속하려면 이 기능을 켜십시오. <0>자세히 알아보기" }, "profile": { "label": "Zoo Code 터미널 프로필", @@ -853,7 +853,7 @@ "advanced": { "diff": { "label": "diff를 통한 편집 활성화", - "description": "활성화되면 Roo는 파일을 더 빠르게 편집할 수 있으며 잘린 전체 파일 쓰기를 자동으로 거부합니다", + "description": "활성화되면 Zoo는 파일을 더 빠르게 편집할 수 있으며 잘린 전체 파일 쓰기를 자동으로 거부합니다", "strategy": { "label": "Diff 전략", "options": { @@ -870,7 +870,7 @@ }, "todoList": { "label": "할 일 목록 도구 활성화", - "description": "활성화하면 Roo가 작업 진행 상황을 추적하기 위한 할 일 목록을 만들고 관리할 수 있습니다. 이는 복잡한 작업을 관리 가능한 단계로 구성하는 데 도움이 됩니다." + "description": "활성화하면 Zoo가 작업 진행 상황을 추적하기 위한 할 일 목록을 만들고 관리할 수 있습니다. 이는 복잡한 작업을 관리 가능한 단계로 구성하는 데 도움이 됩니다." } }, "experimental": { @@ -880,15 +880,15 @@ }, "INSERT_BLOCK": { "name": "실험적 콘텐츠 삽입 도구 사용", - "description": "실험적 콘텐츠 삽입 도구를 활성화하여 Roo가 diff를 만들 필요 없이 특정 줄 번호에 콘텐츠를 삽입할 수 있게 합니다." + "description": "실험적 콘텐츠 삽입 도구를 활성화하여 Zoo가 diff를 만들 필요 없이 특정 줄 번호에 콘텐츠를 삽입할 수 있게 합니다." }, "MULTI_SEARCH_AND_REPLACE": { "name": "실험적 다중 블록 diff 도구 사용", - "description": "활성화하면 Roo가 다중 블록 diff 도구를 사용합니다. 이것은 하나의 요청에서 파일의 여러 코드 블록을 업데이트하려고 시도합니다." + "description": "활성화하면 Zoo가 다중 블록 diff 도구를 사용합니다. 이것은 하나의 요청에서 파일의 여러 코드 블록을 업데이트하려고 시도합니다." }, "CONCURRENT_FILE_READS": { "name": "동시 파일 읽기 활성화", - "description": "활성화하면 Roo가 한 번의 요청으로 여러 파일 을 읽을 수 있습니다. 비활성화하면 Roo는 파일을 하나씩 읽어야 합니다. 성능이 낮은 모델로 작업하거나 파일 액세스를 더 제어하려는 경우 비활성화하면 도움이 될 수 있습니다." + "description": "활성화하면 Zoo가 한 번의 요청으로 여러 파일을 읽을 수 있습니다. 비활성화하면 Zoo는 파일을 하나씩 읽어야 합니다. 성능이 낮은 모델로 작업하거나 파일 액세스를 더 제어하려는 경우 비활성화하면 도움이 될 수 있습니다." }, "MARKETPLACE": { "name": "Marketplace 활성화", @@ -896,7 +896,7 @@ }, "PREVENT_FOCUS_DISRUPTION": { "name": "백그라운드 편집", - "description": "활성화하면 편집기 포커스 방해를 방지합니다. 파일 편집이 diff 뷰를 열거나 포커스를 빼앗지 않고 백그라운드에서 수행됩니다. Roo가 변경사항을 적용하는 동안 방해받지 않고 계속 작업할 수 있습니다. 파일은 진단을 캡처하기 위해 포커스 없이 열거나 완전히 닫힌 상태로 유지할 수 있습니다." + "description": "활성화하면 편집기 포커스 방해를 방지합니다. 파일 편집이 diff 뷰를 열거나 포커스를 빼앗지 않고 백그라운드에서 수행됩니다. Zoo가 변경사항을 적용하는 동안 방해받지 않고 계속 작업할 수 있습니다. 파일은 진단을 캡처하기 위해 포커스 없이 열거나 완전히 닫힌 상태로 유지할 수 있습니다." }, "ASSISTANT_MESSAGE_PARSER": { "name": "새 메시지 파서 사용", @@ -910,7 +910,7 @@ "providerLabel": "제공업체", "providerDescription": "이미지 생성에 사용할 제공업체를 선택하세요.", "name": "AI 이미지 생성 활성화", - "description": "활성화하면 Roo는 OpenRouter의 이미지 생성 모델을 사용하여 텍스트 프롬프트에서 이미지를 생성할 수 있습니다. OpenRouter API 키 구성이 필요합니다.", + "description": "활성화하면 Zoo는 OpenRouter의 이미지 생성 모델을 사용하여 텍스트 프롬프트에서 이미지를 생성할 수 있습니다. OpenRouter API 키 구성이 필요합니다.", "openRouterApiKeyLabel": "OpenRouter API 키", "openRouterApiKeyPlaceholder": "OpenRouter API 키를 입력하세요", "getApiKeyText": "API 키를 받을 곳", @@ -921,11 +921,11 @@ }, "RUN_SLASH_COMMAND": { "name": "모델 시작 슬래시 명령 활성화", - "description": "활성화되면 Roo가 워크플로를 실행하기 위해 슬래시 명령을 실행할 수 있습니다." + "description": "활성화되면 Zoo가 워크플로를 실행하기 위해 슬래시 명령을 실행할 수 있습니다." }, "CUSTOM_TOOLS": { "name": "사용자 정의 도구 활성화", - "description": "활성화하면 Roo가 프로젝트의 .roo/tools 디렉터리 또는 전역 도구를 위한 ~/.roo/tools에서 사용자 정의 TypeScript/JavaScript 도구를 로드하고 사용할 수 있습니다. 참고: 이러한 도구는 자동으로 자동 승인됩니다.", + "description": "활성화하면 Zoo가 프로젝트의 .roo/tools 디렉터리 또는 전역 도구를 위한 ~/.roo/tools에서 사용자 정의 TypeScript/JavaScript 도구를 로드하고 사용할 수 있습니다. 참고: 이러한 도구는 자동으로 자동 승인됩니다.", "toolsHeader": "사용 가능한 사용자 정의 도구", "noTools": "로드된 사용자 정의 도구가 없습니다. 프로젝트의 .roo/tools 디렉터리 또는 전역 도구를 위한 ~/.roo/tools에 .ts 또는 .js 파일을 추가하세요.", "refreshButton": "새로고침", @@ -937,7 +937,7 @@ }, "promptCaching": { "label": "프롬프트 캐싱 비활성화", - "description": "체크하면 Roo가 이 모델에 대해 프롬프트 캐싱을 사용하지 않습니다." + "description": "체크하면 Zoo가 이 모델에 대해 프롬프트 캐싱을 사용하지 않습니다." }, "temperature": { "useCustom": "사용자 정의 온도 사용", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 356b6f618b..aa7608b1f3 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -182,7 +182,7 @@ "current": "Huidig" }, "instructions": { - "wantsToFetch": "Roo wil gedetailleerde instructies ophalen om te helpen met de huidige taak" + "wantsToFetch": "Zoo wil gedetailleerde instructies ophalen om te helpen met de huidige taak" }, "fileOperations": { "wantsToRead": "Zoo wil dit bestand lezen", @@ -192,8 +192,8 @@ "wantsToEditOutsideWorkspace": "Zoo wil dit bestand buiten de werkruimte bewerken", "wantsToEditProtected": "Zoo wil een beveiligd configuratiebestand bewerken", "wantsToCreate": "Zoo wil een nieuw bestand aanmaken", - "wantsToSearchReplace": "Roo wil zoeken en vervangen in dit bestand", - "didSearchReplace": "Roo heeft zoeken en vervangen uitgevoerd op dit bestand", + "wantsToSearchReplace": "Zoo wil zoeken en vervangen in dit bestand", + "didSearchReplace": "Zoo heeft zoeken en vervangen uitgevoerd op dit bestand", "wantsToInsert": "Zoo wil inhoud invoegen in dit bestand", "wantsToInsertWithLineNumber": "Zoo wil inhoud invoegen in dit bestand op regel {{lineNumber}}", "wantsToInsertAtEnd": "Zoo wil inhoud toevoegen aan het einde van dit bestand", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 45b92345d9..6664eb3d74 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "U kunt een globale sneltoets voor deze instelling configureren in de voorkeuren van uw IDE.", - "description": "Sta Roo toe om automatisch handelingen uit te voeren zonder goedkeuring. Schakel deze instellingen alleen in als je de AI volledig vertrouwt en de bijbehorende beveiligingsrisico's begrijpt.", + "description": "Sta Zoo toe om automatisch handelingen uit te voeren zonder goedkeuring. Schakel deze instellingen alleen in als je de AI volledig vertrouwt en de bijbehorende beveiligingsrisico's begrijpt.", "enabled": "Auto-goedkeuren ingeschakeld", "toggleAriaLabel": "Automatisch goedkeuren in-/uitschakelen", "disabledAriaLabel": "Automatisch goedkeuren uitgeschakeld - selecteer eerst opties", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 9cf15a939d..505d8b766c 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -187,7 +187,7 @@ "current": "Bieżący" }, "instructions": { - "wantsToFetch": "Roo chce pobrać szczegółowe instrukcje, aby pomóc w bieżącym zadaniu" + "wantsToFetch": "Zoo chce pobrać szczegółowe instrukcje, aby pomóc w bieżącym zadaniu" }, "fileOperations": { "wantsToRead": "Zoo chce przeczytać ten plik", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo chce edytować ten plik poza obszarem roboczym", "wantsToEditProtected": "Zoo chce edytować chroniony plik konfiguracyjny", "wantsToCreate": "Zoo chce utworzyć nowy plik", - "wantsToSearchReplace": "Roo chce wykonać wyszukiwanie i zamianę w tym pliku", - "didSearchReplace": "Roo wykonał wyszukiwanie i zamianę w tym pliku", + "wantsToSearchReplace": "Zoo chce wykonać wyszukiwanie i zamianę w tym pliku", + "didSearchReplace": "Zoo wykonał wyszukiwanie i zamianę w tym pliku", "wantsToInsert": "Zoo chce wstawić zawartość do tego pliku", "wantsToInsertWithLineNumber": "Zoo chce wstawić zawartość do tego pliku w linii {{lineNumber}}", "wantsToInsertAtEnd": "Zoo chce dodać zawartość na końcu tego pliku", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bf35f445dd..7a7ab3dcb0 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Możesz skonfigurować globalny skrót dla tego ustawienia w preferencjach swojego IDE.", - "description": "Pozwól Roo na automatyczne wykonywanie operacji bez wymagania zatwierdzenia. Włącz te ustawienia tylko jeśli w pełni ufasz AI i rozumiesz związane z tym zagrożenia bezpieczeństwa.", + "description": "Pozwól Zoo na automatyczne wykonywanie operacji bez wymagania zatwierdzenia. Włącz te ustawienia tylko jeśli w pełni ufasz AI i rozumiesz związane z tym zagrożenia bezpieczeństwa.", "enabled": "Auto-zatwierdzanie włączone", "toggleAriaLabel": "Przełącz automatyczne zatwierdzanie", "disabledAriaLabel": "Automatyczne zatwierdzanie wyłączone - najpierw wybierz opcje", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 771d21cd86..45e7b227ce 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -187,7 +187,7 @@ "current": "Atual" }, "instructions": { - "wantsToFetch": "Roo quer buscar instruções detalhadas para ajudar com a tarefa atual" + "wantsToFetch": "Zoo quer buscar instruções detalhadas para ajudar com a tarefa atual" }, "fileOperations": { "wantsToRead": "Zoo quer ler este arquivo", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo quer editar este arquivo fora do espaço de trabalho", "wantsToEditProtected": "Zoo quer editar um arquivo de configuração protegido", "wantsToCreate": "Zoo quer criar um novo arquivo", - "wantsToSearchReplace": "Roo quer realizar busca e substituição neste arquivo", - "didSearchReplace": "Roo realizou busca e substituição neste arquivo", + "wantsToSearchReplace": "Zoo quer realizar busca e substituição neste arquivo", + "didSearchReplace": "Zoo realizou busca e substituição neste arquivo", "wantsToInsert": "Zoo quer inserir conteúdo neste arquivo", "wantsToInsertWithLineNumber": "Zoo quer inserir conteúdo neste arquivo na linha {{lineNumber}}", "wantsToInsertAtEnd": "Zoo quer adicionar conteúdo ao final deste arquivo", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 99d6013a89..bb44c1bcaa 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Você pode configurar um atalho global para esta configuração nas preferências do seu IDE.", - "description": "Permitir que o Roo realize operações automaticamente sem exigir aprovação. Ative essas configurações apenas se confiar totalmente na IA e compreender os riscos de segurança associados.", + "description": "Permitir que o Zoo realize operações automaticamente sem exigir aprovação. Ative essas configurações apenas se confiar totalmente na IA e compreender os riscos de segurança associados.", "enabled": "Aprovação automática habilitada", "toggleAriaLabel": "Alternar aprovação automática", "disabledAriaLabel": "Aprovação automática desativada - selecione as opções primeiro", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 0e1f45ff17..5b8ae0da19 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -182,7 +182,7 @@ "current": "Текущая" }, "instructions": { - "wantsToFetch": "Roo хочет получить подробные инструкции для помощи с текущей задачей" + "wantsToFetch": "Zoo хочет получить подробные инструкции для помощи с текущей задачей" }, "fileOperations": { "wantsToRead": "Zoo хочет прочитать этот файл", @@ -192,8 +192,8 @@ "wantsToEditOutsideWorkspace": "Zoo хочет отредактировать этот файл вне рабочей области", "wantsToEditProtected": "Zoo хочет отредактировать защищённый файл конфигурации", "wantsToCreate": "Zoo хочет создать новый файл", - "wantsToSearchReplace": "Roo хочет выполнить поиск и замену в этом файле", - "didSearchReplace": "Roo выполнил поиск и замену в этом файле", + "wantsToSearchReplace": "Zoo хочет выполнить поиск и замену в этом файле", + "didSearchReplace": "Zoo выполнил поиск и замену в этом файле", "wantsToInsert": "Zoo хочет вставить содержимое в этот файл", "wantsToInsertWithLineNumber": "Zoo хочет вставить содержимое в этот файл на строку {{lineNumber}}", "wantsToInsertAtEnd": "Zoo хочет добавить содержимое в конец этого файла", @@ -242,7 +242,7 @@ "response": "Ответ", "arguments": "Аргументы", "text": { - "rooSaid": "Ру сказал" + "rooSaid": "Zoo сказал" }, "feedback": { "youSaid": "Вы сказали" @@ -382,7 +382,7 @@ "autoApprovedCostLimitReached": { "title": "Достигнут лимит автоматически одобряемых расходов", "button": "Сбросить и продолжить", - "description": "Ру достиг автоматически утвержденного лимита расходов в размере ${{count}}. Хотите сбросить расходы и продолжить выполнение задачи?" + "description": "Zoo достиг автоматически утвержденного лимита расходов в размере ${{count}}. Хотите сбросить расходы и продолжить выполнение задачи?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a29da12326..ee02578534 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Вы можете настроить глобальное сочетание клавиш для этого параметра в настройках вашей IDE.", - "description": "Разрешить Roo автоматически выполнять операции без необходимости одобрения. Включайте эти параметры только если полностью доверяете ИИ и понимаете связанные с этим риски безопасности.", + "description": "Разрешить Zoo автоматически выполнять операции без необходимости одобрения. Включайте эти параметры только если полностью доверяете ИИ и понимаете связанные с этим риски безопасности.", "enabled": "Автоодобрение включено", "toggleAriaLabel": "Переключить автоодобрение", "disabledAriaLabel": "Автоодобрение отключено - сначала выберите опции", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 2e9b107008..a5c0bac756 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -187,7 +187,7 @@ "current": "Mevcut" }, "instructions": { - "wantsToFetch": "Roo mevcut göreve yardımcı olmak için ayrıntılı talimatlar almak istiyor" + "wantsToFetch": "Zoo mevcut göreve yardımcı olmak için ayrıntılı talimatlar almak istiyor" }, "fileOperations": { "wantsToRead": "Zoo bu dosyayı okumak istiyor", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo çalışma alanı dışındaki bu dosyayı düzenlemek istiyor", "wantsToEditProtected": "Zoo korumalı bir yapılandırma dosyasını düzenlemek istiyor", "wantsToCreate": "Zoo yeni bir dosya oluşturmak istiyor", - "wantsToSearchReplace": "Roo bu dosyada arama ve değiştirme yapmak istiyor", - "didSearchReplace": "Roo bu dosyada arama ve değiştirme yaptı", + "wantsToSearchReplace": "Zoo bu dosyada arama ve değiştirme yapmak istiyor", + "didSearchReplace": "Zoo bu dosyada arama ve değiştirme yaptı", "wantsToInsert": "Zoo bu dosyaya içerik eklemek istiyor", "wantsToInsertWithLineNumber": "Zoo bu dosyanın {{lineNumber}}. satırına içerik eklemek istiyor", "wantsToInsertAtEnd": "Zoo bu dosyanın sonuna içerik eklemek istiyor", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 13c0b9c37c..e37c154ead 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "IDE tercihlerinizde bu ayar için genel bir kısayol yapılandırabilirsiniz.", - "description": "Roo'nun onay gerektirmeden otomatik olarak işlemler gerçekleştirmesine izin verin. Bu ayarları yalnızca yapay zekaya tamamen güveniyorsanız ve ilgili güvenlik risklerini anlıyorsanız etkinleştirin.", + "description": "Zoo'nun onay gerektirmeden otomatik olarak işlemler gerçekleştirmesine izin verin. Bu ayarları yalnızca yapay zekaya tamamen güveniyorsanız ve ilgili güvenlik risklerini anlıyorsanız etkinleştirin.", "enabled": "Oto-onay etkinleştirildi", "toggleAriaLabel": "Otomatik onayı değiştir", "disabledAriaLabel": "Otomatik onay devre dışı - önce seçenekleri belirleyin", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 7737ea7b75..b5da186e43 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -187,7 +187,7 @@ "current": "Hiện tại" }, "instructions": { - "wantsToFetch": "Roo muốn lấy hướng dẫn chi tiết để hỗ trợ nhiệm vụ hiện tại" + "wantsToFetch": "Zoo muốn lấy hướng dẫn chi tiết để hỗ trợ nhiệm vụ hiện tại" }, "fileOperations": { "wantsToRead": "Zoo muốn đọc tệp này", @@ -197,8 +197,8 @@ "wantsToEditOutsideWorkspace": "Zoo muốn chỉnh sửa tệp này bên ngoài không gian làm việc", "wantsToEditProtected": "Zoo muốn chỉnh sửa tệp cấu hình được bảo vệ", "wantsToCreate": "Zoo muốn tạo một tệp mới", - "wantsToSearchReplace": "Roo muốn thực hiện tìm kiếm và thay thế trong tệp này", - "didSearchReplace": "Roo đã thực hiện tìm kiếm và thay thế trong tệp này", + "wantsToSearchReplace": "Zoo muốn thực hiện tìm kiếm và thay thế trong tệp này", + "didSearchReplace": "Zoo đã thực hiện tìm kiếm và thay thế trong tệp này", "wantsToInsert": "Zoo muốn chèn nội dung vào tệp này", "wantsToInsertWithLineNumber": "Zoo muốn chèn nội dung vào dòng {{lineNumber}} của tệp này", "wantsToInsertAtEnd": "Zoo muốn thêm nội dung vào cuối tệp này", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b59313d47a..c3b40a004a 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "Bạn có thể định cấu hình một phím tắt chung cho cài đặt này trong tùy chọn IDE của bạn.", - "description": "Cho phép Roo tự động thực hiện các hoạt động mà không cần phê duyệt. Chỉ bật những cài đặt này nếu bạn hoàn toàn tin tưởng AI và hiểu rõ các rủi ro bảo mật liên quan.", + "description": "Cho phép Zoo tự động thực hiện các hoạt động mà không cần phê duyệt. Chỉ bật những cài đặt này nếu bạn hoàn toàn tin tưởng AI và hiểu rõ các rủi ro bảo mật liên quan.", "enabled": "Phê duyệt tự động đã bật", "toggleAriaLabel": "Chuyển đổi tự động phê duyệt", "disabledAriaLabel": "Tự động phê duyệt bị vô hiệu hóa - hãy chọn các tùy chọn trước", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 3322c53542..63b8ac69fd 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -130,7 +130,7 @@ "title": "模式", "marketplace": "模式市场", "settings": "模式设置", - "description": "专门定制Roo行为的角色。", + "description": "专门定制Zoo行为的角色。", "searchPlaceholder": "搜索模式...", "noResults": "未找到结果" }, @@ -144,7 +144,7 @@ "diffError": { "title": "编辑失败" }, - "troubleMessage": "Roo遇到问题...", + "troubleMessage": "Zoo遇到问题...", "apiRequest": { "title": "API请求", "failed": "API请求失败", @@ -187,7 +187,7 @@ "current": "当前" }, "instructions": { - "wantsToFetch": "Roo 想要获取详细指示以协助当前任务" + "wantsToFetch": "Zoo 想要获取详细指示以协助当前任务" }, "fileOperations": { "wantsToRead": "需要读取文件", @@ -253,8 +253,8 @@ "youSaid": "你说" }, "mcp": { - "wantsToUseTool": "Roo想在{{serverName}} MCP上使用工具", - "wantsToAccessResource": "Roo想访问{{serverName}} MCP服务上的资源" + "wantsToUseTool": "Zoo想在{{serverName}} MCP上使用工具", + "wantsToAccessResource": "Zoo想访问{{serverName}} MCP服务上的资源" }, "modes": { "wantsToSwitch": "即将切换至{{mode}}模式", @@ -263,8 +263,8 @@ "didSwitchWithReason": "已切换至{{mode}}模式(原因:{{reason}})" }, "subtasks": { - "wantsToCreate": "Roo想在{{mode}}模式下创建新子任务", - "wantsToFinish": "Roo想完成此子任务", + "wantsToCreate": "Zoo想在{{mode}}模式下创建新子任务", + "wantsToFinish": "Zoo想完成此子任务", "newTaskContent": "子任务说明", "completionContent": "子任务已完成", "resultContent": "子任务结果", @@ -273,7 +273,7 @@ "goToSubtask": "查看任务" }, "questions": { - "hasQuestion": "Roo有一个问题" + "hasQuestion": "Zoo有一个问题" }, "taskCompleted": "任务完成", "modelResponseIncomplete": "模型响应不完整", @@ -381,7 +381,7 @@ }, "autoApprovedCostLimitReached": { "title": "已达到自动批准的费用限额", - "description": "Roo已经达到了${{count}}的自动批准成本限制。您想重置成本并继续任务吗?", + "description": "Zoo已经达到了${{count}}的自动批准成本限制。您想重置成本并继续任务吗?", "button": "重置并继续" } }, diff --git a/webview-ui/src/i18n/locales/zh-CN/prompts.json b/webview-ui/src/i18n/locales/zh-CN/prompts.json index 84255d8868..9d3f9ee9cf 100644 --- a/webview-ui/src/i18n/locales/zh-CN/prompts.json +++ b/webview-ui/src/i18n/locales/zh-CN/prompts.json @@ -9,7 +9,7 @@ "editModesConfig": "模式设置", "editGlobalModes": "修改全局模式", "editProjectModes": "编辑项目模式 (.roomodes)", - "createModeHelpText": "模式是Roo的专属角色,用于定制其行为。<0>了解如何使用模式或<1>自定义模式。", + "createModeHelpText": "模式是Zoo的专属角色,用于定制其行为。<0>了解如何使用模式或<1>自定义模式。", "selectMode": "搜索模式" }, "apiConfiguration": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 1b408875d4..2e8abf435e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -283,7 +283,7 @@ }, "autoApprove": { "toggleShortcut": "您可以在 IDE 首选项中为此设置配置全局快捷方式。", - "description": "允许 Roo 自动执行操作而无需批准。只有在您完全信任 AI 并了解相关安全风险的情况下才启用这些设置。", + "description": "允许 Zoo 自动执行操作而无需批准。只有在您完全信任 AI 并了解相关安全风险的情况下才启用这些设置。", "enabled": "自动批准已启用", "toggleAriaLabel": "切换自动批准", "disabledAriaLabel": "自动批准已禁用 - 请先选择选项", @@ -616,7 +616,7 @@ }, "consecutiveMistakeLimit": { "label": "错误和重复限制", - "description": "在显示“Roo遇到问题”对话框前允许的连续错误或重复操作次数。设置为 0 可禁用此安全机制(它将永远不会触发)。", + "description": "在显示“Zoo遇到问题”对话框前允许的连续错误或重复操作次数。设置为 0 可禁用此安全机制(它将永远不会触发)。", "unlimitedDescription": "已启用无限重试(自动继续)。对话框将永远不会出现。", "warning": "⚠️ 设置为 0 允许无限重试,这可能会消耗大量 API 使用量" }, diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 3807cd2489..dbea150f29 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -228,7 +228,7 @@ "didLoad": "Zoo 載入了技能" }, "instructions": { - "wantsToFetch": "Roo 想要取得詳細指示以協助目前工作" + "wantsToFetch": "Zoo 想要取得詳細指示以協助目前工作" }, "fileOperations": { "wantsToRead": "Zoo 想要讀取此檔案", From 2c987fc71e8451f22d4eb79be5b6481cf7607099 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:23:34 -0400 Subject: [PATCH 74/89] [Fix] Zoo prompts about command output when short foreground commands emit output (#1043) * fix(terminal): delay command_output ask for short foreground commands The command_output ask fired on the first output chunk of any foreground command, prompting users even for commands about to complete. Schedule the ask instead so it only fires when the command is still running after a 5s delay, preserving the interrupt/feedback path for long-running commands while letting short commands finish without prompting. Closes #1042 * fix(terminal): anchor command_output ask delay to execution start Anchor the ask-delay timer to onShellExecutionStarted (falling back to the pre-runCommand timestamp) so shell-integration startup on cold terminals does not consume the grace period, and expand ask-policy tests to cover the agent-timeout cancel, re-anchor reschedule, ask error handling, and non-message responses to restore patch coverage. * test(terminal): cover fallback anchor for command_output ask delay * fix(terminal): clean up pending command_output ask on completion - Supersede a still-pending command_output ask in onCompleted so it resolves immediately instead of lingering until the next interactive message, and clear the Proceed/Kill controls in the webview when the final non-partial command_output say arrives. - Continue the process for any ask answer, not just typed messages, so a non-message response actually backgrounds the command and the tool resolves before the process completes. - Strengthen ask-policy tests: assert the rescheduled ask still fires at the re-anchored deadline, assert early resolution for non-message answers, and cover ask supersession on completion. * test(terminal): note synthetic yesButtonClicked in non-message ask test --------- Co-authored-by: Roomote --- src/core/tools/ExecuteCommandTool.ts | 109 +++++- .../__tests__/executeCommandTool.spec.ts | 336 ++++++++++++++++++ webview-ui/src/components/chat/ChatView.tsx | 11 + .../chat/__tests__/ChatView.spec.tsx | 63 ++++ 4 files changed, 500 insertions(+), 19 deletions(-) diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index 3d6a44ef18..6e8a9becb4 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -35,6 +35,15 @@ export function canRetryShellIntegrationError(error: unknown): error is ShellInt return error instanceof ShellIntegrationError && !error.commandSubmitted } +/** + * Grace period before a foreground command may trigger a `command_output` ask. + * Short commands that emit output and exit within this window never prompt the + * user; the ask only fires when the command is still running once the delay + * elapses, so users can still interrupt or provide feedback on long-running + * commands. + */ +export const COMMAND_OUTPUT_ASK_DELAY_MS = 5_000 + export function getTerminalProviderForExecution(terminalShellIntegrationDisabled: boolean): { terminalProvider: RooTerminalProvider isCmdExeFallback: boolean @@ -340,6 +349,58 @@ export async function executeCommandInTerminal( resolveOnCompleted = resolve }) + // Delay the `command_output` ask so short foreground commands that emit + // output and exit normally never prompt the user. The ask only fires if the + // command is still running once COMMAND_OUTPUT_ASK_DELAY_MS has elapsed + // since execution started, preserving the interrupt/feedback path for + // long-running commands. The anchor is re-based to onShellExecutionStarted + // (falling back to the pre-runCommand timestamp when that event never + // fires) so shell-integration startup on cold terminals does not consume + // the grace period. + let commandStartedAt = 0 + let commandOutputAskTimer: NodeJS.Timeout | undefined + + const askForCommandOutput = async (process: RooTerminalProcess): Promise => { + if (runInBackground || hasAskedForCommandOutput || completed) { + return + } + + // Mark that we've asked to prevent multiple concurrent asks + hasAskedForCommandOutput = true + + try { + const { response, text, images } = await task.ask("command_output", "") + runInBackground = true + + if (response === "messageResponse") { + message = { text, images } + } + + // Any answer means the command should keep running in the background; + // continue the process so the tool resolves now instead of blocking + // until the command actually completes. + process.continue() + } catch (_error) { + // Silently handle ask errors (e.g., "Current ask promise was ignored") + } + } + + const scheduleCommandOutputAsk = (process: RooTerminalProcess): void => { + if (runInBackground || hasAskedForCommandOutput || completed || commandOutputAskTimer) { + return + } + + const remainingDelay = COMMAND_OUTPUT_ASK_DELAY_MS - (Date.now() - commandStartedAt) + + commandOutputAskTimer = setTimeout( + () => { + commandOutputAskTimer = undefined + void askForCommandOutput(process) + }, + Math.max(remainingDelay, 0), + ) + } + const callbacks: RooTerminalCallbacks = { onLine: async (lines: string, process: RooTerminalProcess) => { accumulatedOutput += lines @@ -359,26 +420,19 @@ export async function executeCommandInTerminal( provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) schedulePartialCommandOutputUpdate() - if (runInBackground || hasAskedForCommandOutput) { - return - } - - // Mark that we've asked to prevent multiple concurrent asks - hasAskedForCommandOutput = true - - try { - const { response, text, images } = await task.ask("command_output", "") - runInBackground = true - - if (response === "messageResponse") { - message = { text, images } - process.continue() - } - } catch (_error) { - // Silently handle ask errors (e.g., "Current ask promise was ignored") - } + scheduleCommandOutputAsk(process) }, onCompleted: async (output: string | undefined) => { + clearTimeout(commandOutputAskTimer) + commandOutputAskTimer = undefined + + // If an interactive command_output ask is still pending, supersede it + // so it resolves immediately instead of lingering until the next + // interactive message bumps lastMessageTs. + if (hasAskedForCommandOutput && !runInBackground) { + task.supersedePendingAsk() + } + clearTimeout(pendingCommandOutputEmitTimer) pendingCommandOutputEmitTimer = undefined @@ -412,9 +466,21 @@ export async function executeCommandInTerminal( console.error("[ExecuteCommandTool] Failed to flush final command_output:", error) }) }, - onShellExecutionStarted: (pid: number | undefined) => { + onShellExecutionStarted: (pid: number | undefined, process: RooTerminalProcess) => { const status: CommandExecutionStatus = { executionId, status: "started", pid, command } provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + + // Re-anchor the ask delay to actual execution start so the shell + // integration startup wait does not count against the grace period. + commandStartedAt = Date.now() + + // Output should not precede this event, but if it did, reschedule + // the pending ask against the corrected anchor. + if (commandOutputAskTimer) { + clearTimeout(commandOutputAskTimer) + commandOutputAskTimer = undefined + scheduleCommandOutputAsk(process) + } }, onShellExecutionComplete: (details: ExitCodeDetails) => { const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode } @@ -441,6 +507,8 @@ export async function executeCommandInTerminal( workingDir = terminal.getCurrentWorkingDirectory() } + // Fallback anchor for providers that never fire onShellExecutionStarted. + commandStartedAt = Date.now() const process = terminal.runCommand(command, callbacks) task.terminalProcess = process @@ -462,6 +530,8 @@ export async function executeCommandInTerminal( new Promise((resolve) => { agentTimeoutId = setTimeout(() => { runInBackground = true + clearTimeout(commandOutputAskTimer) + commandOutputAskTimer = undefined process.continue() task.supersedePendingAsk() resolve() @@ -501,6 +571,7 @@ export async function executeCommandInTerminal( } finally { clearTimeout(agentTimeoutId) clearTimeout(userTimeoutId) + clearTimeout(commandOutputAskTimer) clearTimeout(pendingCommandOutputEmitTimer) task.terminalProcess = undefined } diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index 1725154328..c3236f3565 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -8,6 +8,7 @@ import { formatResponse } from "../../prompts/responses" import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../../shared/tools" import { unescapeHtmlEntities } from "../../../utils/text-normalization" import { Terminal } from "../../../integrations/terminal/Terminal" +import type { RooTerminalCallbacks, RooTerminalProcess } from "../../../integrations/terminal/types" // Mock dependencies vitest.mock("execa", () => ({ @@ -77,6 +78,7 @@ describe("executeCommandTool", () => { }, recordToolUsage: vitest.fn().mockReturnValue({} as ToolUsage), recordToolError: vitest.fn(), + supersedePendingAsk: vitest.fn(), providerRef: { deref: vitest.fn().mockResolvedValue({ getState: vitest.fn().mockResolvedValue({ @@ -333,4 +335,338 @@ describe("executeCommandTool", () => { expect(executeCommandModule.resolveAgentTimeoutMs(30)).toBe(30_000) }) }) + + describe("command_output ask policy", () => { + type MockProcess = Promise & { + continue: ReturnType + abort: ReturnType + } + + interface ControllableTerminal { + callbacks: RooTerminalCallbacks | undefined + proc: MockProcess + resolveProcess: () => void + } + + const setupControllableTerminal = async (): Promise => { + const { TerminalRegistry } = await import("../../../integrations/terminal/TerminalRegistry") + const state: ControllableTerminal = { + callbacks: undefined, + proc: undefined as unknown as MockProcess, + resolveProcess: () => {}, + } + const processPromise = new Promise((resolve) => { + state.resolveProcess = resolve + }) + // Mirror real terminal behavior: continue() resolves the wait early + // while the command keeps running in the background. + state.proc = Object.assign(processPromise, { + continue: vitest.fn(() => state.resolveProcess()), + abort: vitest.fn(), + }) + ;(TerminalRegistry.getOrCreateTerminal as ReturnType).mockResolvedValue({ + runCommand: vitest.fn((_cmd: string, callbacks: RooTerminalCallbacks) => { + state.callbacks = callbacks + return state.proc + }), + getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/workspace"), + }) + return state + } + + const handleCommand = (command: string, timeout?: number) => { + mockToolUse.params.command = command + mockToolUse.params.timeout = timeout === undefined ? undefined : String(timeout) + mockToolUse.nativeArgs = timeout === undefined ? { command } : { command, timeout } + + return executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + } + + it("does not ask about command output when a short command emits output and exits normally", async () => { + vitest.useFakeTimers() + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("echo hello") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("hello\n", proc) + await callbacks.onCompleted!("hello\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + + // Advance past the ask delay to prove the scheduled ask was cancelled + // on completion, not merely deferred beyond the test's runtime. + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS + 1_000) + + await handlePromise + + expect(mockCline.ask).not.toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalled() + const result = mockPushToolResult.mock.calls[0][0] + expect(result).toContain("hello") + expect(result).toContain("Exit code: 0") + }) + + it("asks about command output when the command is still running after the ask delay", async () => { + vitest.useFakeTimers() + mockCline.ask.mockResolvedValue({ response: "messageResponse", text: "keep going", images: undefined }) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 60") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("working...\n", proc) + + // First output alone must not trigger the ask. + expect(mockCline.ask).not.toHaveBeenCalled() + + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS) + + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + expect(terminal.proc.continue).toHaveBeenCalled() + + // Further output after the ask must not schedule another ask. + await callbacks.onLine("still working...\n", proc) + expect(mockCline.ask).toHaveBeenCalledTimes(1) + + // Let the command finish so the tool can resolve. + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + await vitest.advanceTimersByTimeAsync(100) + + await handlePromise + + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("anchors the ask delay to execution start so shell integration startup does not consume it", async () => { + vitest.useFakeTimers() + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("echo hello") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + // Simulate a cold terminal spending most of the grace period waiting + // for shell integration before the command actually starts. + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS - 2_000) + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("hello\n", proc) + + // Past the pre-runCommand anchor deadline but well within the window + // measured from execution start: still no ask. + await vitest.advanceTimersByTimeAsync(2_500) + expect(mockCline.ask).not.toHaveBeenCalled() + + await callbacks.onCompleted!("hello\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS + 1_000) + + await handlePromise + + expect(mockCline.ask).not.toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("re-anchors a pending ask when execution start is reported after early output", async () => { + vitest.useFakeTimers() + mockCline.ask.mockResolvedValue({ response: "messageResponse", text: "keep going", images: undefined }) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 60") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + // Output arrives before the execution-started event (defensive case). + await callbacks.onLine("working...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS - 2_000) + callbacks.onShellExecutionStarted!(1234, proc) + + // The pending ask was rescheduled against the new anchor, so the old + // deadline passing must not fire it. + await vitest.advanceTimersByTimeAsync(2_500) + expect(mockCline.ask).not.toHaveBeenCalled() + + // The ask must still fire at the re-anchored deadline — a version + // that cleared the old timer without rescheduling would fail here. + await vitest.advanceTimersByTimeAsync(2_500) + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + + // Let the command finish so the tool can resolve. + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + await vitest.advanceTimersByTimeAsync(100) + + await handlePromise + + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("cancels a pending ask when the agent timeout moves the command to the background", async () => { + vitest.useFakeTimers() + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("npm run dev", 2) + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("server starting...\n", proc) + + // Agent timeout (2s) fires before the ask delay (5s). + await vitest.advanceTimersByTimeAsync(2_000) + expect(terminal.proc.continue).toHaveBeenCalled() + expect(mockCline.supersedePendingAsk).toHaveBeenCalled() + + // Output after the background transition must never schedule an ask. + await callbacks.onLine("listening...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS + 1_000) + expect(mockCline.ask).not.toHaveBeenCalled() + + await handlePromise + + expect(mockPushToolResult).toHaveBeenCalled() + expect(mockPushToolResult.mock.calls[0][0]).toContain("still running") + }) + + it("falls back to the command dispatch time when execution start is never reported", async () => { + vitest.useFakeTimers() + mockCline.ask.mockResolvedValue({ response: "messageResponse", text: "keep going", images: undefined }) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 60") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + // No onShellExecutionStarted: the pre-runCommand anchor applies. + await callbacks.onLine("working...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS) + + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + await vitest.advanceTimersByTimeAsync(100) + + await handlePromise + + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("swallows ask errors without failing the command", async () => { + vitest.useFakeTimers() + mockCline.ask.mockRejectedValue(new Error("Current ask promise was ignored")) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 60") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("working...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS) + + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + expect(terminal.proc.continue).not.toHaveBeenCalled() + + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + await vitest.advanceTimersByTimeAsync(100) + + await handlePromise + + expect(mockPushToolResult).toHaveBeenCalled() + expect(mockPushToolResult.mock.calls[0][0]).toContain("Exit code: 0") + }) + + it("resolves before completion when the ask is answered without a message", async () => { + // Note: in production only messageResponse answers reach a + // command_output ask (Proceed/Kill route through terminalOperation); + // yesButtonClicked is synthetic here to pin the non-message branch. + vitest.useFakeTimers() + mockCline.ask.mockResolvedValue({ response: "yesButtonClicked", text: undefined, images: undefined }) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 60") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("working...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS) + + // Any ask answer backgrounds the command: the process is continued and + // the tool resolves without waiting for the command to complete. + // Note the process promise is never resolved in this test. + await vitest.advanceTimersByTimeAsync(100) + await handlePromise + + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + expect(terminal.proc.continue).toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalled() + expect(mockPushToolResult.mock.calls[0][0]).toContain("still running") + + // Cleanup: let the command finish. + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + }) + + it("supersedes a pending ask when the command completes", async () => { + vitest.useFakeTimers() + mockCline.ask.mockReturnValue(new Promise(() => {})) + const terminal = await setupControllableTerminal() + + const handlePromise = handleCommand("sleep 5") + + await vitest.waitFor(() => expect(terminal.callbacks).toBeDefined()) + const callbacks = terminal.callbacks! + const proc = terminal.proc as unknown as RooTerminalProcess + + callbacks.onShellExecutionStarted!(1234, proc) + await callbacks.onLine("working...\n", proc) + await vitest.advanceTimersByTimeAsync(executeCommandModule.COMMAND_OUTPUT_ASK_DELAY_MS) + + expect(mockCline.ask).toHaveBeenCalledWith("command_output", "") + + // The command completes while the ask is still pending. + await callbacks.onCompleted!("working...\n", proc) + callbacks.onShellExecutionComplete!({ exitCode: 0 }, proc) + terminal.resolveProcess() + await vitest.advanceTimersByTimeAsync(100) + + await handlePromise + + expect(mockCline.supersedePendingAsk).toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalled() + expect(mockPushToolResult.mock.calls[0][0]).toContain("Exit code: 0") + }) + }) }) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 01c4796c16..0410336609 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -462,6 +462,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction { }), ) }) + + it("clears the command_output controls when the final output arrives", async () => { + const { getByTestId, getByRole, queryByRole } = renderChatView() + + // Hydrate state with a command_output ask (Proceed/Kill controls visible) + mockPostMessage({ + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + { + type: "ask", + ask: "command_output", + ts: Date.now() - 1000, + text: "", + partial: false, + }, + ], + }) + + await waitFor(() => { + expect(getByTestId("chat-textarea")).toBeInTheDocument() + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)) + }) + + expect(getByRole("button", { name: "chat:proceedWhileRunning.title" })).toBeInTheDocument() + + // The command completes: the final non-partial command_output say arrives. + mockPostMessage({ + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + { + type: "ask", + ask: "command_output", + ts: Date.now() - 1000, + text: "", + partial: false, + }, + { + type: "say", + say: "command_output", + ts: Date.now(), + text: "done\n", + partial: false, + }, + ], + }) + + await waitFor(() => { + expect(queryByRole("button", { name: "chat:proceedWhileRunning.title" })).not.toBeInTheDocument() + }) + }) }) describe("ChatView - Follow-up Suggestions", () => { From 1ae8b5bed40bf18b97b1e5d05fcc0284c0957f10 Mon Sep 17 00:00:00 2001 From: edelauna <54631123+edelauna@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:12:59 -0400 Subject: [PATCH 75/89] feat(TaskScheduler): introduce semaphore-based task scheduler (Epic 3, Story 3.1) (#1031) * feat(TaskScheduler): introduce TaskScheduler and wire into ClineProvider * feat(Task): keep track of active running task * docs(eslint): updating guidance on how to handle suppression file * fix(ClineProvider): route createTaskWithHistoryItem through taskScheduler * test(e2e): add scheduler regression tests for task resume paths * refactor(ClineProvider): extract scheduleTask helper to deduplicate fire-and-forget scheduling * refactor(ClineProvider): extract scheduleTask helper to deduplicate fire-and-forget scheduling --------- Co-authored-by: Naved Merchant --- AGENTS.md | 12 ++ apps/vscode-e2e/src/fixtures/subtasks.ts | 65 +++++++ apps/vscode-e2e/src/suite/subtasks.test.ts | 139 ++++++++++++++ src/__tests__/provider-delegation.spec.ts | 46 +++-- src/__tests__/single-open-invariant.spec.ts | 86 +++++++++ src/__tests__/task-run-dispatch.spec.ts | 82 +++++++++ src/core/task/Task.ts | 37 +++- src/core/task/TaskScheduler.ts | 52 ++++++ src/core/task/__tests__/Task.dispose.test.ts | 171 +++++++++++++----- src/core/task/__tests__/TaskScheduler.spec.ts | 161 +++++++++++++++++ src/core/webview/ClineProvider.ts | 26 ++- src/eslint-suppressions.json | 2 +- 12 files changed, 807 insertions(+), 72 deletions(-) create mode 100644 src/__tests__/task-run-dispatch.spec.ts create mode 100644 src/core/task/TaskScheduler.ts create mode 100644 src/core/task/__tests__/TaskScheduler.spec.ts diff --git a/AGENTS.md b/AGENTS.md index c32cb8c64b..64085dc087 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,18 @@ This file provides guidance to agents when working with code in this repository. - Settings View Pattern: When working on `SettingsView`, inputs must bind to the local `cachedState`, NOT the live `useExtensionState()`. The `cachedState` acts as a buffer for user edits, isolating them from the `ContextProxy` source-of-truth until the user explicitly clicks "Save". Wiring inputs directly to the live state causes race conditions. - Changesets: Do NOT create `.changeset` files for each commit or code change. Changesets are managed separately by maintainers and should not be generated by agents during normal development. +## ESLint Suppressions + +`src/eslint-suppressions.json` tracks per-file counts of suppressed lint rules. Suppression counts must never increase. When touching a file, prefer reducing its count when the fix is local and low-risk; avoid broad unrelated cleanup. + +When writing new code: + +- Fix lint violations in the new code rather than suppressing them. +- Avoid `as any`; use typed APIs directly (e.g. `RooCodeEventName.X` constants with typed `on()`/`listenerCount()`), or bracket notation (`obj["privateField"]`) to access private members. Prefer precise test doubles or `unknown` with a type guard over double assertions (`as unknown as T`); use double assertions only as a last resort, with a comment explaining why. +- Avoid floating promises; add `void`, `await`, or `.catch()` as appropriate. +- After editing a file, run `pnpm --dir src exec eslint --prune-suppressions --max-warnings=0 ` and confirm the count for that file did not increase. +- If a suppression is truly unavoidable (e.g. `vi.spyOn(Cls.prototype as any, "privateMethod")` where no typed alternative exists), document why in a comment next to the cast. + ## Test Placement Guidance Prefer the narrowest test layer that proves the behavior. This follows standard test-pyramid guidance: keep most coverage in fast, focused tests; add integration tests for cross-module contracts; reserve end-to-end tests for full workflow confidence. diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index 890c243b38..f5dfc813df 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -48,6 +48,15 @@ export const SUBTASK_XPROFILE_SAME_CHILD_RESULT = "Same-profile child completed" export const SUBTASK_XPROFILE_DIFFERENT_CHILD_RESULT = "Different-profile child completed" export const SUBTASK_XPROFILE_PARENT_RESULT = "Sequential cross-profile parent resumed" +// Scheduler regression tests — exercises TaskScheduler + run() dispatch post-CodeRabbit fix. +// Separate markers to avoid collisions with the other subtask fixtures. +const SCHED_STANDALONE_MARKER = "SCHED_STANDALONE_INTERRUPT_RESUME" +const SCHED_COMPLETED_MARKER = "SCHED_COMPLETED_REOPEN" +export const SCHED_STANDALONE_PROMPT = `${SCHED_STANDALONE_MARKER}: Ask the user exactly this follow-up question: What is the square root of 64? After the user answers, complete with only the answer.` +export const SCHED_STANDALONE_FOLLOWUP_ANSWER = "8" +export const SCHED_COMPLETED_PROMPT = `${SCHED_COMPLETED_MARKER}: Complete immediately with the exact result "Scheduler completed task".` +export const SCHED_COMPLETED_RESULT = "Scheduler completed task" + const apiHangChildMatch = new RegExp(SUBTASK_API_HANG_CHILD_MARKER) const requestContains = (req: ChatCompletionRequest, expected: string[]) => { @@ -396,6 +405,62 @@ export function addSubtaskFixtures(mock: InstanceType) { }, }) + // Scheduler regression fixtures: standalone interrupted task resume and completed task reopen. + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SCHED_STANDALONE_MARKER]) && + !requestContains(req, ["call_sched_standalone_followup_001"]) && + !requestContains(req, [`\\n${SCHED_STANDALONE_FOLLOWUP_ANSWER}\\n`]), + }, + response: { + toolCalls: [ + { + name: "ask_followup_question", + arguments: JSON.stringify({ + question: "What is the square root of 64?", + follow_up: [{ text: SCHED_STANDALONE_FOLLOWUP_ANSWER }], + }), + id: "call_sched_standalone_followup_001", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + toolResultContains(req, "call_sched_standalone_followup_001", [SCHED_STANDALONE_FOLLOWUP_ANSWER]) || + requestContains(req, ["call_sched_standalone_followup_001", SCHED_STANDALONE_FOLLOWUP_ANSWER]) || + requestContains(req, [ + SCHED_STANDALONE_MARKER, + `\\n${SCHED_STANDALONE_FOLLOWUP_ANSWER}\\n`, + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SCHED_STANDALONE_FOLLOWUP_ANSWER }), + id: "call_sched_standalone_completion_002", + }, + ], + }, + }) + + mock.addFixture({ + match: { userMessage: new RegExp(SCHED_COMPLETED_MARKER) }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SCHED_COMPLETED_RESULT }), + id: "call_sched_completed_completion_001", + }, + ], + }, + }) + // Interrupted-child-resumes-and-reports-back scenario (#560) mock.addFixture({ match: { diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 641e02f6ac..9e1d0e83cb 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -5,6 +5,10 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { setDefaultSuiteTimeout } from "./test-utils" import { sleep, waitFor, waitUntilCompleted } from "./utils" import { + SCHED_COMPLETED_PROMPT, + SCHED_COMPLETED_RESULT, + SCHED_STANDALONE_FOLLOWUP_ANSWER, + SCHED_STANDALONE_PROMPT, SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER, SUBTASK_ABANDON_PARENT_PROMPT, SUBTASK_API_HANG_CHILD_MARKER, @@ -945,4 +949,139 @@ suite("Roo Code Subtasks", function () { await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) } }) + + // TaskScheduler regression: resumeTask on a completed task must show resume_completed_task ask. + // Before the CodeRabbit fix, createTaskWithHistoryItem bypassed the scheduler and called + // Task.run() via the constructor's startTask: true default, causing run() to call startTask() + // (clearing history) instead of resumeTaskFromHistory(). + test("resumeTask on a completed task presents resume_completed_task ask", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + api.on(RooCodeEventName.Message, messageHandler) + + try { + // Run a task to completion. + const taskId = await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "ask", + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SCHED_COMPLETED_PROMPT, + }), + }) + + assert.strictEqual( + says[taskId]?.find(({ say }) => say === "completion_result")?.text?.trim(), + SCHED_COMPLETED_RESULT, + "Task should complete with expected result", + ) + + // Re-open it via resumeTask — should hit resumeTaskFromHistory(), showing resume_completed_task. + await api.resumeTask(taskId) + + await waitFor( + () => asks[taskId]?.some(({ type, ask }) => type === "ask" && ask === "resume_completed_task") ?? false, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await sleep(500) + } + }) + + // TaskScheduler regression: resumeTask on an interrupted standalone task must show resume_task + // ask and allow the task to complete normally via the scheduler slot. + test("resumeTask on an interrupted standalone task presents resume_task ask and completes", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + api.on(RooCodeEventName.Message, messageHandler) + + let taskId: string | undefined + + try { + taskId = await api.startNewTask({ + configuration: { + mode: "ask", + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SCHED_STANDALONE_PROMPT, + }) + + // Wait until the task pauses at the follow-up question. + await waitFor(() => asks[taskId!]?.some(({ type, ask }) => type === "ask" && ask === "followup") ?? false) + + // Cancel it — the task becomes interrupted. + await api.cancelCurrentTask() + + await waitFor( + () => asks[taskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + ) + + // Resume via scheduler path (createTaskWithHistoryItem). + const askCountBeforeResume = asks[taskId!]?.length ?? 0 + await api.resumeTask(taskId!) + + await waitFor(() => + (asks[taskId!] ?? []) + .slice(askCountBeforeResume) + .some(({ type, ask }) => type === "ask" && ask === "resume_task"), + ) + + // Sending the answer both acknowledges the resume_task ask and answers the pending + // follow-up question from the original task, completing the task. + const completedTaskId = await waitUntilCompleted({ + api, + start: async () => { + await api.sendMessage(SCHED_STANDALONE_FOLLOWUP_ANSWER) + return taskId! + }, + }) + + assert.strictEqual(completedTaskId, taskId, "The resumed standalone task should complete") + assert.strictEqual( + says[taskId!]?.find(({ say }) => say === "completion_result")?.text?.trim(), + SCHED_STANDALONE_FOLLOWUP_ANSWER, + "Task should complete with the follow-up answer as result", + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await sleep(500) + } + }) }) diff --git a/src/__tests__/provider-delegation.spec.ts b/src/__tests__/provider-delegation.spec.ts index 0a9c34b979..0154027753 100644 --- a/src/__tests__/provider-delegation.spec.ts +++ b/src/__tests__/provider-delegation.spec.ts @@ -4,6 +4,7 @@ import { describe, it, expect, vi } from "vitest" import type { HistoryItem } from "@roo-code/types" import { RooCodeEventName } from "@roo-code/types" import { ClineProvider } from "../core/webview/ClineProvider" +import { TaskScheduler } from "../core/task/TaskScheduler" const parentHistoryItem: HistoryItem = { id: "parent-1", @@ -46,13 +47,14 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { const providerEmit = vi.fn() const parentTask = makeParentTask() - const childStart = vi.fn() + const childRun = vi.fn().mockResolvedValue(undefined) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) - const createTask = vi.fn().mockResolvedValue({ taskId: "child-1", start: childStart }) + const createTask = vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn(), run: childRun }) const handleModeSwitch = vi.fn().mockResolvedValue(undefined) const taskHistoryStore = makeStoreStub() const provider = { + taskScheduler: new TaskScheduler(), emit: providerEmit, getCurrentTask: vi.fn(() => parentTask), removeClineFromStack, @@ -70,6 +72,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { initialTodos: [], mode: "code", }) + await Promise.resolve() // drain scheduler microtask so child.run() is invoked expect(child.taskId).toBe("child-1") @@ -98,8 +101,8 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { childIds: expect.arrayContaining(["child-1"]), }) - // child.start() called AFTER parent metadata is persisted - expect(childStart).toHaveBeenCalledTimes(1) + // child.run() called AFTER parent metadata is persisted (via taskScheduler) + expect(childRun).toHaveBeenCalledTimes(1) // Provider-level event expect(providerEmit).toHaveBeenCalledWith(RooCodeEventName.TaskDelegated, "parent-1", "child-1") @@ -117,10 +120,11 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask: vi.fn(() => parentTask), removeClineFromStack: vi.fn().mockResolvedValue(undefined), - createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn() }), + createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn(), run: () => Promise.resolve() }), handleModeSwitch: vi.fn().mockResolvedValue(undefined), postMessageToWebview, log: vi.fn(), @@ -150,10 +154,11 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask: vi.fn(() => parentTask), removeClineFromStack: vi.fn().mockResolvedValue(undefined), - createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn() }), + createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn(), run: () => Promise.resolve() }), handleModeSwitch: vi.fn().mockResolvedValue(undefined), postMessageToWebview, log: vi.fn(), @@ -172,15 +177,15 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { expect(postMessageToWebview).not.toHaveBeenCalled() }) - it("calls child.start() only after atomicReadAndUpdate completes (no race condition)", async () => { + it("calls child.run() only after atomicReadAndUpdate completes (no race condition)", async () => { const callOrder: string[] = [] const parentTask = makeParentTask() - const childStart = vi.fn(() => callOrder.push("child.start")) + const childRun = vi.fn(async () => callOrder.push("child.run")) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTask = vi.fn(async () => { callOrder.push("createTask") - return { taskId: "child-1", start: childStart } + return { taskId: "child-1", start: vi.fn(), run: childRun } }) const handleModeSwitch = vi.fn().mockResolvedValue(undefined) const taskHistoryStore = makeStoreStub({ @@ -191,6 +196,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask: vi.fn(() => parentTask), removeClineFromStack, @@ -208,9 +214,10 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { initialTodos: [], mode: "code", }) + await Promise.resolve() // drain scheduler microtask so child.run() is invoked - // createTask → atomicReadAndUpdate → child.start: lock must release before start - expect(callOrder).toEqual(["createTask", "atomicReadAndUpdate", "child.start"]) + // createTask → atomicReadAndUpdate → child.run: scheduler admits child only after metadata is persisted + expect(callOrder).toEqual(["createTask", "atomicReadAndUpdate", "child.run"]) }) it("implicitly severs interrupted awaited child and re-delegates when parent is already delegated", async () => { @@ -236,10 +243,11 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask: vi.fn(() => makeParentTask()), removeClineFromStack: vi.fn().mockResolvedValue(undefined), - createTask: vi.fn().mockResolvedValue({ taskId: "child-2", start: vi.fn() }), + createTask: vi.fn().mockResolvedValue({ taskId: "child-2", start: vi.fn(), run: () => Promise.resolve() }), handleModeSwitch: vi.fn().mockResolvedValue(undefined), log: vi.fn(), isViewLaunched: false, @@ -277,7 +285,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { delegatedToId: oldChildId, } as unknown as HistoryItem - const child = { taskId: "child-2", start: vi.fn() } + const child = { taskId: "child-2", start: vi.fn(), run: vi.fn().mockResolvedValue(undefined) } const getCurrentTask = vi.fn().mockReturnValue(makeParentTask()) const createTask = vi.fn().mockImplementation(async () => { getCurrentTask.mockReturnValue(child) @@ -296,6 +304,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask, removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -319,15 +328,15 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }), ).rejects.toThrow("Cannot re-delegate") - // Rollback: child must not have started, and must be cleaned up - expect(child.start).not.toHaveBeenCalled() + // Rollback: child must not have run, and must be cleaned up + expect(child.run).not.toHaveBeenCalled() expect((provider as any).deleteTaskWithId).toHaveBeenCalledWith("child-2", false) }) it("rolls back the paused child and restores the parent when atomicReadAndUpdate fails", async () => { const persistError = new Error("parent metadata persist failed") const parentTask = makeParentTask() - const childStart = vi.fn() + const childRun = vi.fn().mockResolvedValue(undefined) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const deleteTaskWithId = vi.fn().mockResolvedValue(undefined) const createTaskWithHistoryItem = vi.fn().mockResolvedValue(undefined) @@ -337,7 +346,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { atomicReadAndUpdate: vi.fn().mockRejectedValue(persistError), }) - const child = { taskId: "child-1", start: childStart } + const child = { taskId: "child-1", start: vi.fn(), run: childRun } // Before createTask: getCurrentTask returns parent (used by step 3 close). // After createTask: returns child so the rollback guard passes and the child is popped. const getCurrentTask = vi.fn().mockReturnValue(parentTask) @@ -347,6 +356,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }) const provider = { + taskScheduler: new TaskScheduler(), emit: vi.fn(), getCurrentTask, removeClineFromStack, @@ -370,7 +380,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { }), ).rejects.toThrow(persistError) - expect(childStart).not.toHaveBeenCalled() + expect(childRun).not.toHaveBeenCalled() expect(removeClineFromStack).toHaveBeenNthCalledWith(1) expect(removeClineFromStack).toHaveBeenNthCalledWith(2) expect(deleteTaskWithId).toHaveBeenCalledWith("child-1", false) diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 0cb9425680..9eeec8a960 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -4,6 +4,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { type OutputChannel } from "vscode" import { ClineProvider } from "../core/webview/ClineProvider" import { TaskRegistry } from "../core/task/TaskRegistry" +import { TaskScheduler } from "../core/task/TaskScheduler" import { type Task } from "../core/task/Task" import { API } from "../extension/api" import * as ProfileValidatorMod from "../shared/ProfileValidator" @@ -45,6 +46,9 @@ vi.mock("../core/task/Task", () => { opts.onCreated?.(this) } start() {} + run() { + return Promise.resolve() + } on() {} off() {} emit() {} @@ -63,12 +67,14 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) + const schedulespy = vi.fn().mockResolvedValue(undefined) const existingTask = { taskId: "existing-1", abort: false, abandoned: false } const registry = new TaskRegistry() registry.push(existingTask as unknown as Task) const provider = { taskRegistry: registry, + taskScheduler: { schedule: schedulespy }, getCurrentTask: vi.fn(() => existingTask), taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), @@ -104,6 +110,7 @@ describe("Single-open-task invariant", () => { expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) + expect(schedulespy).toHaveBeenCalledTimes(1) }) it("Subtask create: keeps existing task open when parentTask is provided", async () => { @@ -117,6 +124,7 @@ describe("Single-open-task invariant", () => { const provider = { taskRegistry: registry2, + taskScheduler: new TaskScheduler(), setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, @@ -152,6 +160,7 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) const updateGlobalState = vi.fn().mockResolvedValue(undefined) + const schedulespy = vi.fn().mockResolvedValue(undefined) const provider = { getCurrentTask: vi.fn(() => undefined), // ensure not rehydrating @@ -180,6 +189,7 @@ describe("Single-open-task invariant", () => { // Methods used by createTaskWithHistoryItem for pending edit cleanup getPendingEditOperation: vi.fn().mockReturnValue(undefined), clearPendingEditOperation: vi.fn(), + taskScheduler: { schedule: schedulespy }, context: { extension: { packageJSON: {} }, globalStorageUri: { fsPath: "/tmp" } }, contextProxy: { extensionUri: {}, @@ -206,6 +216,82 @@ describe("Single-open-task invariant", () => { expect(task).toBeTruthy() expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) + expect(schedulespy).toHaveBeenCalledTimes(1) + }) + + it("History resume path wires scheduler in rehydrating (in-place) case", async () => { + const schedulespy = vi.fn().mockResolvedValue(undefined) + const removeClineFromStack = vi.fn().mockResolvedValue(undefined) + const historyId = "hist-rehydrate-1" + + const existingTask = { + taskId: historyId, + instanceId: "old-inst", + abort: false, + abandoned: false, + abortTask: vi.fn().mockResolvedValue(undefined), + emit: vi.fn(), + } + + const registry = new TaskRegistry() + registry.push(existingTask as unknown as Task) + + const provider = { + getCurrentTask: vi.fn(() => existingTask), + taskRegistry: registry, + taskHistoryStore: { get: vi.fn(() => undefined) }, + markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), + get evictCurrentTask() { + return privateClineProvider.evictCurrentTask.bind(this) + }, + removeClineFromStack, + addClineToStack: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + customModesManager: { getCustomModes: vi.fn().mockResolvedValue([]) }, + providerSettingsManager: { + getModeConfigId: vi.fn().mockResolvedValue(undefined), + listConfig: vi.fn().mockResolvedValue([]), + }, + getState: vi.fn().mockResolvedValue({ + apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, + enableCheckpoints: true, + checkpointTimeout: 60, + experiments: {}, + cloudUserInfo: null, + taskSyncEnabled: false, + }), + getPendingEditOperation: vi.fn().mockReturnValue(undefined), + clearPendingEditOperation: vi.fn(), + taskScheduler: { schedule: schedulespy }, + taskEventListeners: new WeakMap(), + performPreparationTasks: vi.fn().mockResolvedValue(undefined), + context: { extension: { packageJSON: {} }, globalStorageUri: { fsPath: "/tmp" } }, + contextProxy: { + extensionUri: {}, + getValue: vi.fn(), + setValue: vi.fn(), + setProviderSettings: vi.fn(), + getProviderSettings: vi.fn(() => ({})), + }, + postStateToWebview: vi.fn(), + } as unknown as ClineProvider + + const historyItem = { + id: historyId, + number: 1, + ts: Date.now(), + task: "Task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: "/tmp", + } + + await privateClineProvider.createTaskWithHistoryItem.call(provider, historyItem) + + expect(schedulespy).toHaveBeenCalledTimes(1) + // evictCurrentTask must NOT have been called — in-place replace, no stack pop + expect(removeClineFromStack).not.toHaveBeenCalled() }) it("IPC StartNewTask path closes current before new task", async () => { diff --git a/src/__tests__/task-run-dispatch.spec.ts b/src/__tests__/task-run-dispatch.spec.ts new file mode 100644 index 0000000000..283cec3338 --- /dev/null +++ b/src/__tests__/task-run-dispatch.spec.ts @@ -0,0 +1,82 @@ +// npx vitest run __tests__/task-run-dispatch.spec.ts +// +// Tests Task#run() dispatch logic in isolation by calling the method on a +// minimal stand-in object that mirrors only the private fields run() reads. +// This avoids the cost of constructing a real Task (which pulls in VSCode APIs, +// RooIgnoreController, FileContextTracker, etc.). + +import { describe, it, expect, vi } from "vitest" +import { Task } from "../core/task/Task" + +// Access private fields at runtime — TypeScript enforces visibility at +// compile time only; at runtime they're plain properties on the instance. +type Runnable = { + _started: boolean + _runPromise: Promise | undefined + _isHistoryTask: boolean + metadata: { task?: string | null; images?: string[] | null } + resumeTaskFromHistory: () => Promise + startTask: (task?: string, images?: string[]) => Promise +} + +function makeRunnable(overrides: Partial = {}): Runnable & { run(): Promise } { + const obj: Runnable = { + _started: false, + _runPromise: undefined, + _isHistoryTask: false, + metadata: { task: undefined, images: undefined }, + resumeTaskFromHistory: vi.fn().mockResolvedValue(undefined), + startTask: vi.fn().mockResolvedValue(undefined), + ...overrides, + } + // Bind the real run() implementation from Task.prototype to our stand-in. + const runnable = obj as Runnable & { run(): Promise } + runnable.run = Task.prototype.run.bind(obj) + return runnable +} + +describe("Task#run() dispatch", () => { + it("returns the existing _runPromise when already set", async () => { + const sentinel = Promise.resolve() + const obj = makeRunnable({ _runPromise: sentinel }) + expect(obj.run()).toBe(sentinel) + }) + + it("returns Promise.resolve() and skips dispatch when _started is true", async () => { + const obj = makeRunnable({ _started: true }) + const result = obj.run() + await expect(result).resolves.toBeUndefined() + expect(obj.resumeTaskFromHistory).not.toHaveBeenCalled() + expect(obj.startTask).not.toHaveBeenCalled() + }) + + it("calls resumeTaskFromHistory() when _isHistoryTask is true", async () => { + const obj = makeRunnable({ _isHistoryTask: true }) + await obj.run() + expect(obj.resumeTaskFromHistory).toHaveBeenCalledTimes(1) + expect(obj.startTask).not.toHaveBeenCalled() + }) + + it("calls startTask() when _isHistoryTask is false and task text is set", async () => { + const obj = makeRunnable({ _isHistoryTask: false, metadata: { task: "do something" } }) + await obj.run() + expect(obj.startTask).toHaveBeenCalledWith("do something", undefined) + expect(obj.resumeTaskFromHistory).not.toHaveBeenCalled() + }) + + it("returns Promise.resolve() when _isHistoryTask is false and no task/images", async () => { + const obj = makeRunnable({ _isHistoryTask: false, metadata: { task: undefined, images: undefined } }) + await obj.run() + expect(obj.startTask).not.toHaveBeenCalled() + expect(obj.resumeTaskFromHistory).not.toHaveBeenCalled() + }) + + it("returns the same promise on repeated calls (idempotent via _runPromise)", async () => { + const obj = makeRunnable({ _isHistoryTask: true }) + const p1 = obj.run() + const p2 = obj.run() + expect(p1).toBe(p2) + await p1 + expect(obj.resumeTaskFromHistory).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 43774eccc5..8f69a3a0d4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -406,6 +406,8 @@ export class Task extends EventEmitter implements TaskLike { didToolFailInCurrentTurn = false didCompleteReadingStream = false private _started = false + private _runPromise: Promise | undefined + private readonly _isHistoryTask: boolean // No streaming parser is required. assistantMessageParser?: undefined private providerProfileChangeListener?: (config: { name: string; provider?: string }) => void @@ -488,6 +490,7 @@ export class Task extends EventEmitter implements TaskLike { task: historyItem ? historyItem.task : task, images: historyItem ? [] : images, } + this._isHistoryTask = !!historyItem && !task && !images // Normal use-case is usually retry similar history task with new workspace. this.workspacePath = parentTask @@ -1372,7 +1375,7 @@ export class Task extends EventEmitter implements TaskLike { // Wait for askResponse to be set await pWaitFor( () => { - if (this.askResponse !== undefined || this.lastMessageTs !== askTs) { + if (this.abort || this.askResponse !== undefined || this.lastMessageTs !== askTs) { return true } @@ -1397,6 +1400,11 @@ export class Task extends EventEmitter implements TaskLike { { interval: 100 }, ) + /* v8 ignore next 3 -- abort-while-waiting path; covered by e2e standalone-resume test */ + if (this.abort) { + throw new Error(`[ZooCode#ask] task ${this.taskId}.${this.instanceId} aborted`) + } + if (this.lastMessageTs !== askTs) { // Could happen if we send multiple asks in a row i.e. with // command_output. It's important that when we know an ask could @@ -1880,6 +1888,31 @@ export class Task extends EventEmitter implements TaskLike { } } + /** + * Like `start()`, but returns the underlying promise so callers (e.g. + * `TaskScheduler`) can await task completion and gate concurrency. + * Idempotent: subsequent calls return the same in-flight promise. + */ + public run(): Promise { + if (this._runPromise !== undefined) { + return this._runPromise + } + if (this._started) { + // Already launched via constructor or start() — no promise to return. + return Promise.resolve() + } + this._started = true + + const { task, images } = this.metadata + + this._runPromise = this._isHistoryTask + ? this.resumeTaskFromHistory() + : task || images + ? this.startTask(task ?? undefined, images ?? undefined) + : Promise.resolve() + return this._runPromise + } + private async startTask(task?: string, images?: string[]): Promise { try { // `conversationHistory` (for API) and `clineMessages` (for webview) @@ -2003,7 +2036,7 @@ export class Task extends EventEmitter implements TaskLike { .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // Could be multiple resume tasks. let askType: ClineAsk - if (lastClineMessage?.ask === "completion_result") { + if (this.initialStatus === "completed" || lastClineMessage?.ask === "completion_result") { askType = "resume_completed_task" } else { askType = "resume_task" diff --git a/src/core/task/TaskScheduler.ts b/src/core/task/TaskScheduler.ts new file mode 100644 index 0000000000..7431fb76ad --- /dev/null +++ b/src/core/task/TaskScheduler.ts @@ -0,0 +1,52 @@ +import { TaskSemaphore } from "../../utils/TaskSemaphore" +import { type Task } from "./Task" + +/** + * Semaphore-based concurrency gate for task execution. + * + * Ships at maxConcurrency=1, which is structurally identical to the current + * serial behavior. Raising maxConcurrency later enables Story 3.2b fan-out + * without touching the gate logic here. + */ +export class TaskScheduler { + private readonly sem: TaskSemaphore + + constructor(maxConcurrency = 1) { + this.sem = new TaskSemaphore(maxConcurrency) + } + + get waiting(): number { + return this.sem.waiting + } + + /** + * Acquire a permit for `task`, call `run()`, and release on completion. + * + * The returned promise resolves/rejects with the same value as `run()`. + * Release is guaranteed via try/finally even if `run()` throws. + * + * If the task was aborted or abandoned while waiting for a permit (e.g. the + * user cancelled it before it started), the permit is released immediately + * without calling `run()`. + */ + async schedule(task: Task, run: () => Promise): Promise { + const release = await this.sem.acquire() + if (task.abort || task.abandoned) { + release() + return + } + try { + await run() + } finally { + release() + } + } + + /** + * Cancel all queued (waiting) tasks. Tasks that already acquired a permit + * are not affected — they continue to run to completion. + */ + cancelQueued(): void { + this.sem.cancel() + } +} diff --git a/src/core/task/__tests__/Task.dispose.test.ts b/src/core/task/__tests__/Task.dispose.test.ts index 9d61cd67c1..bc14edb366 100644 --- a/src/core/task/__tests__/Task.dispose.test.ts +++ b/src/core/task/__tests__/Task.dispose.test.ts @@ -1,4 +1,4 @@ -import { ProviderSettings } from "@roo-code/types" +import { type ProviderSettings, RooCodeEventName } from "@roo-code/types" import { Task } from "../Task" import { ClineProvider } from "../../webview/ClineProvider" @@ -44,7 +44,11 @@ vi.mock("@roo-code/telemetry", () => ({ })) describe("Task dispose method", () => { - let mockProvider: any + let mockProvider: { + context: { globalStorageUri: { fsPath: string } } + getState: ReturnType + log: ReturnType + } let mockApiConfiguration: ProviderSettings let task: Task @@ -69,7 +73,7 @@ describe("Task dispose method", () => { // Create task instance without starting it task = new Task({ - provider: mockProvider as ClineProvider, + provider: mockProvider as unknown as ClineProvider, apiConfiguration: mockApiConfiguration, startTask: false, }) @@ -88,15 +92,14 @@ describe("Task dispose method", () => { const listener2 = vi.fn(() => {}) const listener3 = vi.fn((taskId: string) => {}) - // Use type assertion to bypass strict event typing for testing - ;(task as any).on("TaskStarted", listener1) - ;(task as any).on("TaskAborted", listener2) - ;(task as any).on("TaskIdle", listener3) + task.on(RooCodeEventName.TaskStarted, listener1) + task.on(RooCodeEventName.TaskAborted, listener2) + task.on(RooCodeEventName.TaskIdle, listener3) // Verify listeners are added - expect(task.listenerCount("TaskStarted")).toBe(1) - expect(task.listenerCount("TaskAborted")).toBe(1) - expect(task.listenerCount("TaskIdle")).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskStarted)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskAborted)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskIdle)).toBe(1) // Spy on removeAllListeners method const removeAllListenersSpy = vi.spyOn(task, "removeAllListeners") @@ -108,9 +111,9 @@ describe("Task dispose method", () => { expect(removeAllListenersSpy).toHaveBeenCalledOnce() // Verify all listeners are removed - expect(task.listenerCount("TaskStarted")).toBe(0) - expect(task.listenerCount("TaskAborted")).toBe(0) - expect(task.listenerCount("TaskIdle")).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskStarted)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskAborted)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskIdle)).toBe(0) }) test("should handle errors when removing event listeners", () => { @@ -158,53 +161,125 @@ describe("Task dispose method", () => { const listeners = { TaskStarted: vi.fn(() => {}), TaskAborted: vi.fn(() => {}), - TaskIdle: vi.fn((taskId: string) => {}), - TaskActive: vi.fn((taskId: string) => {}), + TaskIdle: vi.fn((_taskId: string) => {}), + TaskActive: vi.fn((_taskId: string) => {}), TaskAskResponded: vi.fn(() => {}), - Message: vi.fn((data: { action: "created" | "updated"; message: any }) => {}), - TaskTokenUsageUpdated: vi.fn((taskId: string, tokenUsage: any) => {}), - TaskToolFailed: vi.fn((taskId: string, tool: any, error: string) => {}), - TaskUnpaused: vi.fn(() => {}), + Message: vi.fn(() => {}), + TaskTokenUsageUpdated: vi.fn(() => {}), + TaskToolFailed: vi.fn(() => {}), + TaskUnpaused: vi.fn((_taskId: string) => {}), } - // Add all listeners using type assertion to bypass strict typing for testing - const taskAny = task as any - taskAny.on("TaskStarted", listeners.TaskStarted) - taskAny.on("TaskAborted", listeners.TaskAborted) - taskAny.on("TaskIdle", listeners.TaskIdle) - taskAny.on("TaskActive", listeners.TaskActive) - taskAny.on("TaskAskResponded", listeners.TaskAskResponded) - taskAny.on("Message", listeners.Message) - taskAny.on("TaskTokenUsageUpdated", listeners.TaskTokenUsageUpdated) - taskAny.on("TaskToolFailed", listeners.TaskToolFailed) - taskAny.on("TaskUnpaused", listeners.TaskUnpaused) + task.on(RooCodeEventName.TaskStarted, listeners.TaskStarted) + task.on(RooCodeEventName.TaskAborted, listeners.TaskAborted) + task.on(RooCodeEventName.TaskIdle, listeners.TaskIdle) + task.on(RooCodeEventName.TaskActive, listeners.TaskActive) + task.on(RooCodeEventName.TaskAskResponded, listeners.TaskAskResponded) + task.on(RooCodeEventName.Message, listeners.Message) + task.on(RooCodeEventName.TaskTokenUsageUpdated, listeners.TaskTokenUsageUpdated) + task.on(RooCodeEventName.TaskToolFailed, listeners.TaskToolFailed) + task.on(RooCodeEventName.TaskUnpaused, listeners.TaskUnpaused) // Verify all listeners are added - expect(task.listenerCount("TaskStarted")).toBe(1) - expect(task.listenerCount("TaskAborted")).toBe(1) - expect(task.listenerCount("TaskIdle")).toBe(1) - expect(task.listenerCount("TaskActive")).toBe(1) - expect(task.listenerCount("TaskAskResponded")).toBe(1) - expect(task.listenerCount("Message")).toBe(1) - expect(task.listenerCount("TaskTokenUsageUpdated")).toBe(1) - expect(task.listenerCount("TaskToolFailed")).toBe(1) - expect(task.listenerCount("TaskUnpaused")).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskStarted)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskAborted)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskIdle)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskActive)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskAskResponded)).toBe(1) + expect(task.listenerCount(RooCodeEventName.Message)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskTokenUsageUpdated)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskToolFailed)).toBe(1) + expect(task.listenerCount(RooCodeEventName.TaskUnpaused)).toBe(1) // Call dispose task.dispose() // Verify all listeners are removed - expect(task.listenerCount("TaskStarted")).toBe(0) - expect(task.listenerCount("TaskAborted")).toBe(0) - expect(task.listenerCount("TaskIdle")).toBe(0) - expect(task.listenerCount("TaskActive")).toBe(0) - expect(task.listenerCount("TaskAskResponded")).toBe(0) - expect(task.listenerCount("Message")).toBe(0) - expect(task.listenerCount("TaskTokenUsageUpdated")).toBe(0) - expect(task.listenerCount("TaskToolFailed")).toBe(0) - expect(task.listenerCount("TaskUnpaused")).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskStarted)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskAborted)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskIdle)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskActive)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskAskResponded)).toBe(0) + expect(task.listenerCount(RooCodeEventName.Message)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskTokenUsageUpdated)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskToolFailed)).toBe(0) + expect(task.listenerCount(RooCodeEventName.TaskUnpaused)).toBe(0) // Verify total listener count is 0 expect(task.eventNames().length).toBe(0) }) }) + +describe("Task.run() idempotency", () => { + // Reuses the mock setup from the outer describe block above. + let mockProvider: ReturnType + let mockApiConfiguration: ProviderSettings + + function buildMockProvider() { + return { + context: { globalStorageUri: { fsPath: "/test/path" } }, + getState: vi.fn().mockResolvedValue({ mode: "code" }), + log: vi.fn(), + } + } + + beforeEach(() => { + vi.clearAllMocks() + mockProvider = buildMockProvider() + mockApiConfiguration = { apiProvider: "anthropic", apiKey: "test-key" } as ProviderSettings + }) + + test("run() does not invoke startTask when task was already started by constructor", async () => { + // Spy on the prototype before construction so we capture the constructor's call too. + const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockResolvedValue(undefined) + + const t = new Task({ + provider: mockProvider as unknown as ClineProvider, + apiConfiguration: mockApiConfiguration, + task: "hello", + startTask: true, + }) + + const callsBefore = startTaskSpy.mock.calls.length // constructor fired it once + void t.run() + expect(startTaskSpy.mock.calls.length).toBe(callsBefore) // run() must not add a second call + t.dispose() + startTaskSpy.mockRestore() + }) + + test("run() does not invoke startTask when task was already started by start()", async () => { + const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockResolvedValue(undefined) + + const t = new Task({ + provider: mockProvider as unknown as ClineProvider, + apiConfiguration: mockApiConfiguration, + task: "hello", + startTask: false, + }) + t.start() + const callsAfterStart = startTaskSpy.mock.calls.length // start() fired it once + + void t.run() + expect(startTaskSpy.mock.calls.length).toBe(callsAfterStart) // no additional call + t.dispose() + startTaskSpy.mockRestore() + }) + + test("run() returns the same promise on repeated calls", async () => { + const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockResolvedValue(undefined) + + const t = new Task({ + provider: mockProvider as unknown as ClineProvider, + apiConfiguration: mockApiConfiguration, + task: "hello", + startTask: false, + }) + + const p1 = t.run() + const p2 = t.run() + expect(p1).toBe(p2) + await p1 + t.dispose() + startTaskSpy.mockRestore() + }) +}) diff --git a/src/core/task/__tests__/TaskScheduler.spec.ts b/src/core/task/__tests__/TaskScheduler.spec.ts new file mode 100644 index 0000000000..9a0a4f5701 --- /dev/null +++ b/src/core/task/__tests__/TaskScheduler.spec.ts @@ -0,0 +1,161 @@ +import { TaskScheduler } from "../TaskScheduler" +import { type Task } from "../Task" + +const stubTask = () => ({}) as unknown as Task + +describe("TaskScheduler", () => { + it("runs a task immediately when a permit is available", async () => { + const scheduler = new TaskScheduler(1) + let ran = false + await scheduler.schedule(stubTask(), async () => { + ran = true + }) + expect(ran).toBe(true) + }) + + it("queues a second task at maxConcurrency=1 until the first completes", async () => { + const scheduler = new TaskScheduler(1) + const order: number[] = [] + let resolveFirst!: () => void + + const first = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveFirst = res))) + // Give the microtask queue a chance to acquire the permit. + await Promise.resolve() + + expect(scheduler.waiting).toBe(0) + + const second = scheduler.schedule(stubTask(), async () => { + order.push(2) + }) + await Promise.resolve() + expect(scheduler.waiting).toBe(1) + + order.push(1) + resolveFirst() + await first + await second + + expect(order).toEqual([1, 2]) + }) + + it("allows two tasks in parallel at maxConcurrency=2", async () => { + const scheduler = new TaskScheduler(2) + const running: number[] = [] + let resolveA: (() => void) | undefined + let resolveB: (() => void) | undefined + + const a = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveA = res))) + const b = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveB = res))) + + // Two microtask ticks: one for sem.acquire() in each schedule() call. + await Promise.resolve() + await Promise.resolve() + + expect(scheduler.waiting).toBe(0) + expect(resolveA).toBeDefined() + expect(resolveB).toBeDefined() + running.push(1, 2) + resolveA!() + resolveB!() + await Promise.all([a, b]) + expect(running).toEqual([1, 2]) + }) + + it("releases the permit even when the run function throws", async () => { + const scheduler = new TaskScheduler(1) + await expect( + scheduler.schedule(stubTask(), async () => { + throw new Error("boom") + }), + ).rejects.toThrow("boom") + + // Permit must have been released — next task should run immediately. + let ran = false + await scheduler.schedule(stubTask(), async () => { + ran = true + }) + expect(ran).toBe(true) + }) + + it("cancelQueued() rejects waiting tasks without affecting the running one", async () => { + const scheduler = new TaskScheduler(1) + let resolveRunning!: () => void + const running = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveRunning = res))) + + await Promise.resolve() + + const errors: unknown[] = [] + const queued = scheduler.schedule(stubTask(), async () => {}).catch((e) => errors.push(e)) + await Promise.resolve() + + expect(scheduler.waiting).toBe(1) + scheduler.cancelQueued() + expect(scheduler.waiting).toBe(0) + + await queued + expect(errors).toHaveLength(1) + + // Running task is unaffected. + resolveRunning() + await expect(running).resolves.toBeUndefined() + }) + + it("skips run() and releases permit when task is aborted before admission", async () => { + const scheduler = new TaskScheduler(1) + let resolveFirst!: () => void + const first = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveFirst = res))) + await Promise.resolve() + + const abortedTask = { abort: true, abandoned: false } as unknown as Task + let ran = false + const queued = scheduler.schedule(abortedTask, async () => { + ran = true + }) + await Promise.resolve() + expect(scheduler.waiting).toBe(1) + + resolveFirst() + await Promise.all([first, queued]) + + expect(ran).toBe(false) + // Permit must be released — a subsequent task can run immediately. + let next = false + await scheduler.schedule(stubTask(), async () => { + next = true + }) + expect(next).toBe(true) + }) + + it("skips run() and releases permit when task is abandoned before admission", async () => { + const scheduler = new TaskScheduler(1) + let resolveFirst!: () => void + const first = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveFirst = res))) + await Promise.resolve() + + const abandonedTask = { abort: false, abandoned: true } as unknown as Task + let ran = false + const queued = scheduler.schedule(abandonedTask, async () => { + ran = true + }) + await Promise.resolve() + + resolveFirst() + await Promise.all([first, queued]) + + expect(ran).toBe(false) + }) + + it("defaults to maxConcurrency=1", async () => { + const scheduler = new TaskScheduler() + let resolveFirst!: () => void + const first = scheduler.schedule(stubTask(), () => new Promise((res) => (resolveFirst = res))) + await Promise.resolve() + + const second = scheduler.schedule(stubTask(), async () => {}) + await Promise.resolve() + expect(scheduler.waiting).toBe(1) + + resolveFirst() + await Promise.all([first, second]) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 28870fc446..2ee92edebc 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -54,6 +54,7 @@ import { } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" import { TaskRegistry } from "../task/TaskRegistry" +import { TaskScheduler } from "../task/TaskScheduler" import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts" import { TelemetryService } from "@roo-code/telemetry" import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" @@ -153,6 +154,12 @@ function runDelegationTransition( return current } +function scheduleTask(scheduler: TaskScheduler, task: Task, source: string): void { + void scheduler + .schedule(task, () => task.run()) + .catch((error) => console.error(`[${source}] taskScheduler.schedule failed:`, error)) +} + export class ClineProvider extends EventEmitter implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike @@ -167,6 +174,7 @@ export class ClineProvider private webviewDisposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel private taskRegistry = new TaskRegistry() + private taskScheduler = new TaskScheduler() private delegationTransitionLocks?: Map> private cancelledDelegationChildIds = new Set() private codeIndexStatusSubscription?: vscode.Disposable @@ -683,6 +691,10 @@ export class ClineProvider this._disposed = true this.log("Disposing ClineProvider...") + // Reject any tasks still waiting for a scheduler permit so they don't + // hold the event loop after the provider is torn down. + this.taskScheduler.cancelQueued() + // Clear all tasks from the stack. The first pop goes through evictCurrentTask() // so an active delegated child is marked interrupted before the extension shuts down, // rather than being left persisted as "active" across the reload. @@ -1182,7 +1194,7 @@ export class ClineProvider taskNumber: historyItem.number, workspacePath: historyItem.workspace, onCreated: this.taskCreationCallback, - startTask: options?.startTask ?? true, + startTask: false, // Preserve the status from the history item to avoid overwriting it when the task saves messages initialStatus: historyItem.status, rateLimitClock: this.rateLimitClock, @@ -1222,12 +1234,20 @@ export class ClineProvider this.log( `[createTaskWithHistoryItem] rehydrated task ${task.taskId}.${task.instanceId} in-place (flicker-free)`, ) + + if (options?.startTask !== false) { + scheduleTask(this.taskScheduler, task, "createTaskWithHistoryItem") + } } else { await this.addClineToStack(task) this.log( `[createTaskWithHistoryItem] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`, ) + + if (options?.startTask !== false) { + scheduleTask(this.taskScheduler, task, "createTaskWithHistoryItem") + } } // Check if there's a pending edit after checkpoint restoration @@ -3186,7 +3206,7 @@ export class ClineProvider await this.addClineToStack(task) if (options.startTask !== false) { - task.start() + scheduleTask(this.taskScheduler, task, "createTask") } this.log( @@ -3698,7 +3718,7 @@ export class ClineProvider } // 6) Start the child task now that parent metadata is safely persisted. - child.start() + scheduleTask(this.taskScheduler, child, "delegateParentAndOpenChild") // 7) Emit TaskDelegated (provider-level) try { diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 1f5e90a12d..608e190d04 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -846,7 +846,7 @@ }, "core/task/__tests__/Task.dispose.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 8 + "count": 3 } }, "core/task/__tests__/Task.persistence.spec.ts": { From dcaa3cbf33e441d49d3083177a70cdb089be377a Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:47:11 -0400 Subject: [PATCH 76/89] [Chore] Harden E2E workflow against VS Code binary download failures (#1045) * ci: harden E2E workflow against VS Code binary download failures Add restore-keys fallback to the VS Code test binary cache, probe the VS Code update API before running tests, fall back to a stale cached binary (via VSCODE_VERSION) when the CDN is unreachable, and retry the mocked E2E step so transient network blips don't fail the merge queue. Skip writing the pass marker when the stale-binary fallback was used. Closes #1044 * ci: scope E2E retries to binary download, probe CDN endpoint, prune stale binaries Address review feedback on #1045: - Retry only the VS Code binary download (its own step), so genuine test failures fail fast instead of re-running the whole suite. - Probe the archive CDN endpoint (via the 302-redirect HEAD) in addition to the update API, so the stale-binary fallback also engages when only the CDN host is down. - Restore/save the binary cache with restore+save actions and prune older vscode-linux-x64-* dirs so each saved entry keeps one binary instead of growing with every version bump; skip the save when the stale fallback was used. - Emit a ::warning:: when endpoints are unreachable with no cached binary, and echo why the pass marker is skipped on fallback runs. --------- Co-authored-by: Roomote Co-authored-by: edelauna <54631123+edelauna@users.noreply.github.com> --- .github/workflows/e2e.yml | 105 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index eba2426d02..2d1819b4e1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -45,9 +45,100 @@ jobs: VERSION=$(node -p 'require("./apps/vscode-e2e/package.json").devDependencies["@types/vscode"]') echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Cache VS Code test binary + - name: Restore VS Code test binary cache if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + id: vscode-cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + apps/vscode-e2e/.vscode-test/ + key: vscode-test-${{ runner.os }}-${{ steps.vscode-ver.outputs.version }}-v1 + # Fall back to the most recent stale binary so a version bump or cache + # eviction doesn't force a download when the VS Code CDN is unreachable. + restore-keys: | + vscode-test-${{ runner.os }}- + + - name: Probe VS Code download endpoints + if: (github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true') && steps.vscode-cache.outputs.cache-hit != 'true' + id: vscode-probe + # The version-resolution API and the archive CDN are different hosts. The + # archive URL 302-redirects to the CDN, so a HEAD probe that follows the + # redirect covers both. The fallback engages when either is unreachable. + run: | + API_OK=false + CDN_OK=false + if curl -sf --max-time 10 https://update.code.visualstudio.com/api/releases/stable > /dev/null; then + API_OK=true + fi + if curl -sfIL --max-time 20 -o /dev/null "https://update.code.visualstudio.com/${{ steps.vscode-ver.outputs.version }}/linux-x64/stable"; then + CDN_OK=true + fi + if [ "$API_OK" = "true" ] && [ "$CDN_OK" = "true" ]; then + echo "reachable=true" >> "$GITHUB_OUTPUT" + else + echo "reachable=false" >> "$GITHUB_OUTPUT" + echo "::warning::VS Code download endpoints unreachable (api=$API_OK, cdn=$CDN_OK); will try the stale cached binary" + fi + + # @vscode/test-electron only skips its live version-resolution request when the + # requested version already exists in .vscode-test/. On an exact-key miss it calls + # the update API before its download retry loop, so an unreachable CDN fails the + # job before any test runs. When the endpoints are down, point the runner at the + # stale binary restored above (runTest.ts honors VSCODE_VERSION) so the suite + # still runs. + - name: Fall back to stale VS Code binary when CDN is unreachable + if: (github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true') && steps.vscode-cache.outputs.cache-hit != 'true' && steps.vscode-probe.outputs.reachable == 'false' + id: vscode-fallback + run: | + STALE=$(ls -d apps/vscode-e2e/.vscode-test/vscode-linux-x64-* 2>/dev/null | sort -V | tail -n 1 || true) + if [ -n "$STALE" ]; then + echo "VSCODE_VERSION=${STALE##*-}" >> "$GITHUB_ENV" + echo "used=true" >> "$GITHUB_OUTPUT" + echo "VS Code download endpoints are unreachable; falling back to cached VS Code ${STALE##*-}" + else + echo "::warning::VS Code download endpoints are unreachable and no cached binary is available; the download step will retry but may fail" + fi + + # Retry only the binary download: version resolution and the archive fetch are + # the network-fragile parts. Genuine test failures should fail fast, so the + # test step itself is deliberately not retried. + - name: Download VS Code test binary + if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' + id: vscode-download + run: | + cd apps/vscode-e2e + for attempt in 1 2 3; do + if node -e "const { downloadAndUnzipVSCode } = require('@vscode/test-electron'); const version = process.env.VSCODE_VERSION || require('./package.json').devDependencies['@types/vscode']; downloadAndUnzipVSCode(version).catch((err) => { console.error(err); process.exit(1); });"; then + exit 0 + fi + echo "VS Code download attempt ${attempt} failed" + if [ "$attempt" -lt 3 ]; then + sleep 15 + fi + done + exit 1 + + # restore-keys can bring back older binaries alongside the one just + # downloaded; keep only the effective version so each saved cache entry + # stays at one binary instead of growing with every version bump. + - name: Prune stale VS Code binaries + if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' + run: | + EFFECTIVE="${VSCODE_VERSION:-${{ steps.vscode-ver.outputs.version }}}" + for dir in apps/vscode-e2e/.vscode-test/vscode-linux-x64-*; do + [ -e "$dir" ] || continue + if [ "$(basename "$dir")" != "vscode-linux-x64-$EFFECTIVE" ]; then + echo "Pruning stale VS Code binary $(basename "$dir")" + rm -rf "$dir" + fi + done + + # Skip the save when the stale-binary fallback ran: the pinned-version key + # must not be populated with an older binary. + - name: Save VS Code test binary cache + if: (github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true') && steps.vscode-cache.outputs.cache-hit != 'true' && steps.vscode-fallback.outputs.used != 'true' + continue-on-error: true + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | apps/vscode-e2e/.vscode-test/ @@ -59,12 +150,18 @@ jobs: if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' run: xvfb-run -a pnpm --filter @roo-code/vscode-e2e test:ci:mock + - name: Explain skipped mocked E2E pass marker + if: steps.e2e-marker.outputs.cache-hit != 'true' && steps.run-e2e.outcome == 'success' && steps.vscode-fallback.outputs.used == 'true' + run: echo "Skipping mocked E2E pass marker because tests ran against a stale cached VS Code binary (VS Code download endpoints unreachable)." + - name: Write mocked E2E pass marker - if: steps.e2e-marker.outputs.cache-hit != 'true' && steps.run-e2e.outcome == 'success' + # Skip when the stale-binary fallback ran: a pass against an older VS Code + # must not mint a marker for the intended-version source hash. + if: steps.e2e-marker.outputs.cache-hit != 'true' && steps.run-e2e.outcome == 'success' && steps.vscode-fallback.outputs.used != 'true' run: mkdir -p .cache/e2e-pass && date -u > .cache/e2e-pass/passed - name: Save mocked E2E pass marker - if: steps.e2e-marker.outputs.cache-hit != 'true' && steps.run-e2e.outcome == 'success' + if: steps.e2e-marker.outputs.cache-hit != 'true' && steps.run-e2e.outcome == 'success' && steps.vscode-fallback.outputs.used != 'true' continue-on-error: true uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: From 088b7eafc0b1e024b736ac82af58710d624103ae Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Thu, 30 Jul 2026 03:06:59 +0300 Subject: [PATCH 77/89] refactor(webview): use canonical provider identifiers (#1023) * refactor(webview): use canonical provider identifiers * test(webview): remove brittle identifier mutation tests * fix(webview): handle remaining model providers explicitly * test(webview): avoid unreachable coverage branch * refactor(webview): use canonical anthropic identifier * test(webview): cover Anthropic Opus 1M tier * test(webview): use canonical Anthropic identifier * fix(webview): address canonical provider review * test(webview): use canonical provider identifiers * test(webview): cover Kimi Code model selection --- .../hooks/__tests__/useSelectedModel.spec.ts | 178 +++++++++++++----- .../components/ui/hooks/useSelectedModel.ts | 76 ++++---- .../src/utils/__tests__/validate.spec.ts | 2 +- webview-ui/src/utils/validate.ts | 51 ++--- 4 files changed, 196 insertions(+), 111 deletions(-) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 9433bb8148..5fca23ba8e 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -8,6 +8,7 @@ import type { Mock } from "vitest" import { ProviderSettings, ModelInfo, + anthropicModels, BEDROCK_1M_CONTEXT_MODEL_IDS, litellmDefaultModelInfo, kenariDefaultModelId, @@ -22,6 +23,8 @@ import { vscodeLlmDefaultModelId, moonshotDefaultModelId, moonshotModels, + kimiCodeDefaultModelInfo, + providerIdentifiers, } from "@roo-code/types" import { useSelectedModel } from "../useSelectedModel" @@ -87,7 +90,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", openRouterSpecificProvider: "test-provider", } @@ -148,7 +151,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", // This model doesn't exist in available models openRouterSpecificProvider: "test-provider", } @@ -211,7 +214,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", openRouterSpecificProvider: "test-provider", } @@ -263,7 +266,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", } @@ -304,7 +307,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "non-existent-model", openRouterSpecificProvider: "non-existent-provider", } @@ -404,7 +407,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) - expect(result.current.provider).toBe("openrouter") + expect(result.current.provider).toBe(providerIdentifiers.openrouter) expect(result.current.id).toBe(openRouterDefaultModelId) expect(result.current.info).toBeUndefined() }) @@ -427,7 +430,7 @@ describe("useSelectedModel", () => { it("should apply 1M pricing tier for Claude Sonnet 4.6 when enabled", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-sonnet-4-6", anthropicBeta1MContext: true, } @@ -440,6 +443,39 @@ describe("useSelectedModel", () => { expect(result.current.info?.inputPrice).toBe(6.0) expect(result.current.info?.outputPrice).toBe(22.5) }) + + it("should apply 1M pricing tier for Claude Opus 4.6 when enabled", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-opus-4-6", + anthropicBeta1MContext: true, + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.id).toBe("claude-opus-4-6") + expect(result.current.info?.contextWindow).toBe(1_000_000) + expect(result.current.info?.inputPrice).toBe(10.0) + expect(result.current.info?.outputPrice).toBe(37.5) + }) + + it.each([providerIdentifiers.anthropic, providerIdentifiers.geminiCli, providerIdentifiers.fakeAi] as const)( + "should explicitly resolve configured models for %s", + (apiProvider) => { + const apiConfiguration: ProviderSettings = { + apiProvider, + apiModelId: "claude-sonnet-4-6", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(apiProvider) + expect(result.current.id).toBe("claude-sonnet-4-6") + expect(result.current.info).toEqual(anthropicModels["claude-sonnet-4-6"]) + }, + ) }) describe("bedrock provider with 1M context", () => { @@ -463,7 +499,7 @@ describe("useSelectedModel", () => { it("should enable 1M context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is true", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0], awsBedrock1MContext: true, } @@ -477,7 +513,7 @@ describe("useSelectedModel", () => { it("should use default context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is false", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0], awsBedrock1MContext: false, } @@ -491,7 +527,7 @@ describe("useSelectedModel", () => { it("should not affect context window for non-Claude Sonnet 4 Bedrock models", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", awsBedrock1MContext: true, } @@ -525,7 +561,7 @@ describe("useSelectedModel", () => { it("should enable supportsPromptCache for custom-arn model", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "custom-arn", } @@ -538,7 +574,7 @@ describe("useSelectedModel", () => { it("should enable supportsImages for custom-arn model", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "custom-arn", } @@ -571,14 +607,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "some-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // Should preserve configured model ID since "some-model" doesn't exist in empty litellm models expect(result.current.id).toBe("some-model") // Should use litellmDefaultModelInfo as fallback @@ -597,14 +633,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, // litellmModelId intentionally omitted } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // LiteLLM has no inherent default; with nothing configured the ID is empty rather than a phantom model expect(result.current.id).toBe("") expect(result.current.info).toEqual(litellmDefaultModelInfo) @@ -632,7 +668,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "my-custom-model", } @@ -677,14 +713,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "non-existing-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // Falls back to default model ID expect(result.current.id).toBe("claude-3-7-sonnet-20250219") // Should use litellmDefaultModelInfo as fallback since default model also not in router models @@ -713,14 +749,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "custom-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) expect(result.current.id).toBe("custom-model") expect(result.current.info).toEqual(customModelInfo) }) @@ -758,14 +794,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "kenari", + apiProvider: providerIdentifiers.kenari, kenariModelId: "glm-5-2", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("kenari") + expect(result.current.provider).toBe(providerIdentifiers.kenari) expect(result.current.id).toBe("glm-5-2") expect(result.current.info).toEqual(customModelInfo) }) @@ -783,14 +819,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "kenari", + apiProvider: providerIdentifiers.kenari, kenariModelId: "some-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("kenari") + expect(result.current.provider).toBe(providerIdentifiers.kenari) // Falls back to the kenari default model ID when the router list is empty expect(result.current.id).toBe(kenariDefaultModelId) // Should use kenariDefaultModelInfo as fallback @@ -819,14 +855,14 @@ describe("useSelectedModel", () => { it("should use openAiModelInfoSaneDefaults when no custom model info is provided", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "gpt-4o", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("gpt-4o") expect(result.current.info).toEqual(openAiModelInfoSaneDefaults) }) @@ -843,7 +879,7 @@ describe("useSelectedModel", () => { } const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "custom-model", openAiCustomModelInfo: customModelInfo, } @@ -851,7 +887,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("custom-model") expect(result.current.info).toEqual(customModelInfo) }) @@ -865,7 +901,7 @@ describe("useSelectedModel", () => { } const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "custom-model-no-tools", openAiCustomModelInfo: customModelInfo, } @@ -873,7 +909,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("custom-model-no-tools") expect(result.current.info).toEqual(customModelInfo) }) @@ -900,27 +936,27 @@ describe("useSelectedModel", () => { it("should return default minimax model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "minimax", + apiProvider: providerIdentifiers.minimax, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("minimax") + expect(result.current.provider).toBe(providerIdentifiers.minimax) expect(result.current.id).toBe(minimaxDefaultModelId) expect(result.current.info).toEqual(minimaxModels[minimaxDefaultModelId]) }) it("should use custom model ID and info when model exists in minimaxModels", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "minimax", + apiProvider: providerIdentifiers.minimax, apiModelId: "MiniMax-M2.7", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("minimax") + expect(result.current.provider).toBe(providerIdentifiers.minimax) expect(result.current.id).toBe("MiniMax-M2.7") expect(result.current.info).toEqual(minimaxModels["MiniMax-M2.7"]) }) @@ -948,14 +984,14 @@ describe("useSelectedModel", () => { it("resolves a listed family's contextWindow to its maxInputTokens", () => { const family = vscodeLlmDefaultModelId const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family }, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("vscode-lm") + expect(result.current.provider).toBe(providerIdentifiers.vscodeLm) expect(result.current.id).toBe(`copilot/${family}`) // The bar and the condense gate share one source of truth: contextWindow === maxInputTokens. expect(result.current.info?.contextWindow).toBe(vscodeLlmModels[family].maxInputTokens) @@ -967,14 +1003,14 @@ describe("useSelectedModel", () => { // the advertised window would be caught here. const family = "claude-opus-4.8" const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family }, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("vscode-lm") + expect(result.current.provider).toBe(providerIdentifiers.vscodeLm) expect(result.current.id).toBe(`copilot/${family}`) expect(result.current.info?.contextWindow).toBe(vscodeLlmModels[family].maxInputTokens) // 197897 expect(result.current.info?.contextWindow).not.toBe(vscodeLlmModels[family].contextWindow) // NOT 679560 @@ -983,7 +1019,7 @@ describe("useSelectedModel", () => { it("falls back to the default model's window for an unlisted family (NOT 128000)", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family: "totally-unknown-family" }, } @@ -1018,32 +1054,76 @@ describe("useSelectedModel", () => { it("should return default Friendli model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "friendli", + apiProvider: providerIdentifiers.friendli, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("friendli") + expect(result.current.provider).toBe(providerIdentifiers.friendli) expect(result.current.id).toBe(friendliDefaultModelId) expect(result.current.info).toEqual(friendliModels[friendliDefaultModelId]) }) it("should use custom model ID and info when model exists in friendliModels", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "friendli", + apiProvider: providerIdentifiers.friendli, apiModelId: "zai-org/GLM-5.1", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("friendli") + expect(result.current.provider).toBe(providerIdentifiers.friendli) expect(result.current.id).toBe("zai-org/GLM-5.1") expect(result.current.info).toEqual(friendliModels["zai-org/GLM-5.1"]) }) }) + describe("Kimi Code provider", () => { + it("should resolve the configured model from router models", () => { + const modelInfo: ModelInfo = { + ...kimiCodeDefaultModelInfo, + description: "Configured Kimi Code model", + } + + mockUseRouterModels.mockReturnValue({ + data: { "kimi-code": { "kimi-for-coding": modelInfo } }, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.kimiCode, + apiModelId: "kimi-for-coding", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(providerIdentifiers.kimiCode) + expect(result.current.id).toBe("kimi-for-coding") + expect(result.current.info).toEqual(modelInfo) + }) + }) + + it("should reject providers unsupported by model selection", () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined) + const preventExpectedError = (event: ErrorEvent) => event.preventDefault() + window.addEventListener("error", preventExpectedError) + const apiConfiguration = { + apiProvider: "unsupported-provider", + } as unknown as ProviderSettings + + const wrapper = createWrapper() + + expect(() => renderHook(() => useSelectedModel(apiConfiguration), { wrapper })).toThrow( + "Unsupported provider: unsupported-provider", + ) + window.removeEventListener("error", preventExpectedError) + consoleError.mockRestore() + }) + describe("moonshot provider", () => { beforeEach(() => { mockUseRouterModels.mockReturnValue({ @@ -1066,13 +1146,13 @@ describe("useSelectedModel", () => { it("should return default moonshot model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("moonshot") + expect(result.current.provider).toBe(providerIdentifiers.moonshot) expect(result.current.id).toBe(moonshotDefaultModelId) expect(result.current.info).toEqual(moonshotModels[moonshotDefaultModelId]) }) @@ -1101,7 +1181,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, } const wrapper = createWrapper() @@ -1114,7 +1194,7 @@ describe("useSelectedModel", () => { it("should fallback to default when model ID is not in static or router models", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, apiModelId: "non-existent-model", } @@ -1127,7 +1207,7 @@ describe("useSelectedModel", () => { it("should use getValidatedModelId to return apiModelId when valid", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, apiModelId: "kimi-k2-turbo-preview", } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 50886d4538..ec513ce885 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -36,6 +36,7 @@ import { isDynamicProvider, isRetiredProvider, getProviderDefaultModelId, + providerIdentifiers, } from "@roo-code/types" import { useRouterModels } from "./useRouterModels" @@ -148,7 +149,7 @@ function getSelectedModel({ // this gives a better UX than showing the default model const defaultModelId = getProviderDefaultModelId(provider) switch (provider) { - case "openrouter": { + case providerIdentifiers.openrouter: { const id = getValidatedModelId(apiConfiguration.openRouterModelId, routerModels.openrouter, defaultModelId) let info = routerModels.openrouter?.[id] const specificProvider = apiConfiguration.openRouterSpecificProvider @@ -164,17 +165,17 @@ function getSelectedModel({ return { id, info } } - case "requesty": { + case providerIdentifiers.requesty: { const id = getValidatedModelId(apiConfiguration.requestyModelId, routerModels.requesty, defaultModelId) const routerInfo = routerModels.requesty?.[id] return { id, info: routerInfo } } - case "unbound": { + case providerIdentifiers.unbound: { const id = getValidatedModelId(apiConfiguration.unboundModelId, routerModels.unbound, defaultModelId) const routerInfo = routerModels.unbound?.[id] return { id, info: routerInfo } } - case "litellm": { + case providerIdentifiers.litellm: { // When the model list is empty (not yet loaded or still loading), // preserve the configured model ID. LiteLLM is a proxy with no inherent // default model, so we never substitute a hardcoded default here -- when @@ -187,17 +188,17 @@ function getSelectedModel({ const routerInfo = routerModels.litellm?.[id] return { id, info: routerInfo ?? litellmDefaultModelInfo } } - case "xai": { + case providerIdentifiers.xai: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = xaiModels[id as keyof typeof xaiModels] return info ? { id, info } : { id, info: undefined } } - case "baseten": { + case providerIdentifiers.baseten: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = basetenModels[id as keyof typeof basetenModels] return { id, info } } - case "bedrock": { + case providerIdentifiers.bedrock: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = bedrockModels[id as keyof typeof bedrockModels] @@ -221,7 +222,7 @@ function getSelectedModel({ return { id, info: baseInfo } } - case "vertex": { + case providerIdentifiers.vertex: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = vertexModels[id as keyof typeof vertexModels] @@ -244,12 +245,12 @@ function getSelectedModel({ return { id, info: baseInfo } } - case "gemini": { + case providerIdentifiers.gemini: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = geminiModels[id as keyof typeof geminiModels] return { id, info } } - case "deepseek": { + case providerIdentifiers.deepseek: { const availableModels = routerModels.deepseek ? { ...deepSeekModels, ...routerModels.deepseek } : deepSeekModels @@ -258,7 +259,7 @@ function getSelectedModel({ const staticInfo = deepSeekModels[id as keyof typeof deepSeekModels] return { id, info: routerInfo ?? staticInfo } } - case "moonshot": { + case providerIdentifiers.moonshot: { const availableModels = routerModels.moonshot ? { ...moonshotModels, ...routerModels.moonshot } : moonshotModels @@ -267,23 +268,23 @@ function getSelectedModel({ const staticInfo = moonshotModels[id as keyof typeof moonshotModels] return { id, info: routerInfo ?? staticInfo } } - case "kimi-code": { + case providerIdentifiers.kimiCode: { const configuredId = apiConfiguration.apiModelId const availableModels = routerModels["kimi-code"] const id = configuredId || defaultModelId return { id, info: availableModels?.[id] ?? kimiCodeDefaultModelInfo } } - case "minimax": { + case providerIdentifiers.minimax: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = minimaxModels[id as keyof typeof minimaxModels] return { id, info } } - case "mimo": { + case providerIdentifiers.mimo: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = mimoModels[id as keyof typeof mimoModels] ?? mimoModels["mimo-v2.5-pro"] return { id, info } } - case "zai": { + case providerIdentifiers.zai: { const isChina = apiConfiguration.zaiApiLine === "china_coding" const models = isChina ? mainlandZAiModels : internationalZAiModels const defaultModelId = getProviderDefaultModelId(provider, { isChina }) @@ -291,23 +292,23 @@ function getSelectedModel({ const info = models[id as keyof typeof models] return { id, info } } - case "openai-native": { + case providerIdentifiers.openaiNative: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = openAiNativeModels[id as keyof typeof openAiNativeModels] return { id, info } } - case "mistral": { + case providerIdentifiers.mistral: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = mistralModels[id as keyof typeof mistralModels] return { id, info } } - case "openai": { + case providerIdentifiers.openai: { const id = apiConfiguration.openAiModelId ?? "" const customInfo = apiConfiguration?.openAiCustomModelInfo const info = customInfo ?? openAiModelInfoSaneDefaults return { id, info } } - case "ollama": { + case providerIdentifiers.ollama: { const id = apiConfiguration.ollamaModelId ?? "" const info = ollamaModels && ollamaModels[apiConfiguration.ollamaModelId!] @@ -323,7 +324,7 @@ function getSelectedModel({ info: adjustedInfo || undefined, } } - case "lmstudio": { + case providerIdentifiers.lmstudio: { const id = apiConfiguration.lmStudioModelId ?? "" const modelInfo = lmStudioModels && lmStudioModels[apiConfiguration.lmStudioModelId!] return { @@ -331,7 +332,7 @@ function getSelectedModel({ info: modelInfo ? { ...lMStudioDefaultModelInfo, ...modelInfo } : undefined, } } - case "vscode-lm": { + case providerIdentifiers.vscodeLm: { const id = apiConfiguration?.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}` : vscodeLlmDefaultModelId @@ -351,37 +352,37 @@ function getSelectedModel({ } return { id, info } } - case "sambanova": { + case providerIdentifiers.sambanova: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = sambaNovaModels[id as keyof typeof sambaNovaModels] return { id, info } } - case "fireworks": { + case providerIdentifiers.fireworks: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = fireworksModels[id as keyof typeof fireworksModels] return { id, info } } - case "friendli": { + case providerIdentifiers.friendli: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = friendliModels[id as keyof typeof friendliModels] return { id, info } } - case "poe": { + case providerIdentifiers.poe: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = routerModels.poe?.[id] return { id, info } } - case "qwen-code": { + case providerIdentifiers.qwenCode: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = qwenCodeModels[id as keyof typeof qwenCodeModels] return { id, info } } - case "openai-codex": { + case providerIdentifiers.openaiCodex: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = openAiCodexModels[id as keyof typeof openAiCodexModels] return { id, info } } - case "vercel-ai-gateway": { + case providerIdentifiers.vercelAiGateway: { const id = getValidatedModelId( apiConfiguration.vercelAiGatewayModelId, routerModels["vercel-ai-gateway"], @@ -390,7 +391,7 @@ function getSelectedModel({ const info = routerModels["vercel-ai-gateway"]?.[id] return { id, info } } - case "opencode-go": { + case providerIdentifiers.opencodeGo: { const id = getValidatedModelId( apiConfiguration.opencodeGoModelId, routerModels["opencode-go"], @@ -401,14 +402,14 @@ function getSelectedModel({ const info = routerModels["opencode-go"]?.[id] ?? opencodeGoDefaultModelInfo return { id, info } } - case "kenari": { + case providerIdentifiers.kenari: { const id = getValidatedModelId(apiConfiguration.kenariModelId, routerModels["kenari"], defaultModelId) // Fall back to the provider's default ModelInfo so capability-driven UI // keeps working when the /models list is empty or unavailable. const info = routerModels["kenari"]?.[id] ?? kenariDefaultModelInfo return { id, info } } - case "zoo-gateway": { + case providerIdentifiers.zooGateway: { const id = getValidatedModelId( apiConfiguration.zooGatewayModelId, routerModels["zoo-gateway"], @@ -417,16 +418,15 @@ function getSelectedModel({ const info = routerModels["zoo-gateway"]?.[id] return { id, info } } - // case "anthropic": - // case "fake-ai": - default: { - provider satisfies "anthropic" | "gemini-cli" | "fake-ai" + case providerIdentifiers.anthropic: + case providerIdentifiers.geminiCli: + case providerIdentifiers.fakeAi: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = anthropicModels[id as keyof typeof anthropicModels] // Apply 1M context beta tier pricing for supported Claude 4 models if ( - provider === "anthropic" && + provider === providerIdentifiers.anthropic && (id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5" || id === "claude-sonnet-4-6" || @@ -461,5 +461,9 @@ function getSelectedModel({ return { id, info: baseInfo } } + default: { + provider satisfies never + throw new Error(`Unsupported provider: ${provider}`) + } } } diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 8aebe3f8e5..6ce9bf5245 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -1,4 +1,4 @@ -import type { ProviderSettings, OrganizationAllowList, RouterModels } from "@roo-code/types" +import { type ProviderSettings, type OrganizationAllowList, type RouterModels } from "@roo-code/types" // Mock i18next to return translation keys with interpolated values vi.mock("i18next", () => ({ diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 0a812e9d48..b750d8833b 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -11,6 +11,7 @@ import { isDynamicProvider, isFauxProvider, isCustomProvider, + providerIdentifiers, } from "@roo-code/types" export function validateApiConfiguration( @@ -42,117 +43,117 @@ function validateModelsAndKeysProvided( zooCodeIsAuthenticated?: boolean, ): string | undefined { switch (apiConfiguration.apiProvider) { - case "openrouter": + case providerIdentifiers.openrouter: if (!apiConfiguration.openRouterApiKey) { return i18next.t("settings:validation.apiKey") } break - case "requesty": + case providerIdentifiers.requesty: if (!apiConfiguration.requestyApiKey) { return i18next.t("settings:validation.apiKey") } break - case "unbound": + case providerIdentifiers.unbound: if (!apiConfiguration.unboundApiKey) { return i18next.t("settings:validation.apiKey") } break - case "litellm": + case providerIdentifiers.litellm: if (!apiConfiguration.litellmApiKey) { return i18next.t("settings:validation.apiKey") } break - case "anthropic": + case providerIdentifiers.anthropic: if (!apiConfiguration.apiKey) { return i18next.t("settings:validation.apiKey") } break - case "bedrock": + case providerIdentifiers.bedrock: if (!apiConfiguration.awsRegion) { return i18next.t("settings:validation.awsRegion") } break - case "vertex": + case providerIdentifiers.vertex: if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { return i18next.t("settings:validation.googleCloud") } break - case "gemini": + case providerIdentifiers.gemini: if (!apiConfiguration.geminiApiKey) { return i18next.t("settings:validation.apiKey") } break - case "openai-native": + case providerIdentifiers.openaiNative: if (!apiConfiguration.openAiNativeApiKey) { return i18next.t("settings:validation.apiKey") } break - case "mistral": + case providerIdentifiers.mistral: if (!apiConfiguration.mistralApiKey) { return i18next.t("settings:validation.apiKey") } break - case "openai": + case providerIdentifiers.openai: if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { return i18next.t("settings:validation.openAi") } break - case "ollama": + case providerIdentifiers.ollama: if (!apiConfiguration.ollamaModelId) { return i18next.t("settings:validation.modelId") } break - case "lmstudio": + case providerIdentifiers.lmstudio: if (!apiConfiguration.lmStudioModelId) { return i18next.t("settings:validation.modelId") } break - case "vscode-lm": + case providerIdentifiers.vscodeLm: if (!apiConfiguration.vsCodeLmModelSelector) { return i18next.t("settings:validation.modelSelector") } break - case "fireworks": + case providerIdentifiers.fireworks: if (!apiConfiguration.fireworksApiKey) { return i18next.t("settings:validation.apiKey") } break - case "friendli": + case providerIdentifiers.friendli: if (!apiConfiguration.friendliApiKey) { return i18next.t("settings:validation.apiKey") } break - case "qwen-code": + case providerIdentifiers.qwenCode: if (!apiConfiguration.qwenCodeOauthPath) { return i18next.t("settings:validation.qwenCodeOauthPath") } break - case "kimi-code": + case providerIdentifiers.kimiCode: if ((apiConfiguration.kimiCodeAuthMethod ?? "oauth") === "api-key" && !apiConfiguration.kimiCodeApiKey) { return i18next.t("settings:validation.apiKey") } break - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: if (!apiConfiguration.vercelAiGatewayApiKey) { return i18next.t("settings:validation.apiKey") } break - case "opencode-go": + case providerIdentifiers.opencodeGo: if (!apiConfiguration.opencodeGoApiKey) { return i18next.t("settings:validation.apiKey") } break - case "kenari": + case providerIdentifiers.kenari: if (!apiConfiguration.kenariApiKey) { return i18next.t("settings:validation.apiKey") } break - case "zoo-gateway": + case providerIdentifiers.zooGateway: if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { return i18next.t("settings:validation.zooGatewaySignIn") } break - case "baseten": + case providerIdentifiers.baseten: if (!apiConfiguration.basetenApiKey) { return i18next.t("settings:validation.apiKey") } @@ -206,7 +207,7 @@ function validateProviderAgainstOrganizationSettings( } function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: ProviderName): string | undefined { - if (provider === "vscode-lm") { + if (provider === providerIdentifiers.vscodeLm) { return apiConfiguration.vsCodeLmModelSelector?.id } @@ -317,7 +318,7 @@ export function validateApiConfigurationExcludingModelErrors( _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, ): string | undefined { - if (apiConfiguration.apiProvider !== "zoo-gateway") { + if (apiConfiguration.apiProvider !== providerIdentifiers.zooGateway) { const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) if (keysAndIdsPresentErrorMessage) { From c37819386119af449e41e97f943c24d0248acb75 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Thu, 30 Jul 2026 03:17:07 +0300 Subject: [PATCH 78/89] refactor: finish canonical provider identifier audit (#1030) * refactor: finish canonical provider identifier audit * fix: address provider identifier review feedback * refactor: centralize Anthropic protocol value * refactor: reuse canonical provider protocol constants * refactor: centralize OpenAI protocol value * test: use canonical keyless provider identifiers * test: address protocol routing review feedback --- .../src/__tests__/provider-settings.test.ts | 118 ++++++++++-------- packages/types/src/provider-settings.ts | 44 +++++-- packages/types/src/providers/anthropic.ts | 1 + packages/types/src/providers/openai.ts | 1 + .../__tests__/checkExistApiConfig.spec.ts | 18 ++- src/shared/checkExistApiConfig.ts | 13 +- 6 files changed, 127 insertions(+), 68 deletions(-) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 724fc20f34..6b61f6fd9f 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,107 +1,123 @@ -import { getApiProtocol } from "../provider-settings.js" +import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js" describe("getApiProtocol", () => { - describe("Anthropic-style providers", () => { - it("should return 'anthropic' for anthropic provider", () => { - expect(getApiProtocol("anthropic")).toBe("anthropic") - expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic") - }) + it("preserves API protocol wire values", () => { + expect(ANTHROPIC_API_PROTOCOL).toBe("anthropic") + expect(OPENAI_API_PROTOCOL).toBe("openai") + }) - it("should return 'anthropic' for bedrock provider", () => { - expect(getApiProtocol("bedrock")).toBe("anthropic") - expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic") - expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic") - }) + describe("Anthropic-style providers", () => { + it.each([providerIdentifiers.anthropic, providerIdentifiers.bedrock, providerIdentifiers.minimax])( + "should return 'anthropic' for %s provider", + (provider) => { + expect(getApiProtocol(provider)).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(provider, "gpt-4")).toBe(ANTHROPIC_API_PROTOCOL) + }, + ) }) describe("Vertex provider with Claude models", () => { it("should return 'anthropic' for vertex provider with claude models", () => { - expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vertex", "Claude-3-Sonnet")).toBe("anthropic") - expect(getApiProtocol("vertex", "CLAUDE-instant")).toBe("anthropic") - expect(getApiProtocol("vertex", "anthropic/claude-3-haiku")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "Claude-3-Sonnet")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-instant")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "anthropic/claude-3-haiku")).toBe(ANTHROPIC_API_PROTOCOL) }) it("should return 'openai' for vertex provider with non-claude models", () => { - expect(getApiProtocol("vertex", "gpt-4")).toBe("openai") - expect(getApiProtocol("vertex", "gemini-pro")).toBe("openai") - expect(getApiProtocol("vertex", "llama-2")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex, "gpt-4")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "gemini-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "llama-2")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for vertex provider without model", () => { - expect(getApiProtocol("vertex")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex)).toBe(OPENAI_API_PROTOCOL) }) }) - describe("Vercel AI Gateway provider", () => { + describe("Gateway providers", () => { + it("uses the canonical Zoo Gateway identifier for Anthropic model protocol selection", () => { + expect(getApiProtocol(providerIdentifiers.zooGateway, "anthropic/claude-3-opus")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + }) + it("should return 'anthropic' for vercel-ai-gateway provider with anthropic models", () => { - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3.5-sonnet")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "ANTHROPIC/claude-sonnet-4")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-opus-4.1")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3-opus")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3.5-sonnet")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "ANTHROPIC/claude-sonnet-4")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-opus-4.1")).toBe( + ANTHROPIC_API_PROTOCOL, + ) }) it("should return 'openai' for vercel-ai-gateway provider with non-anthropic models", () => { - expect(getApiProtocol("vercel-ai-gateway", "openai/gpt-4")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "google/gemini-pro")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "meta/llama-3")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "mistral/mixtral")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "openai/gpt-4")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "google/gemini-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "meta/llama-3")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "mistral/mixtral")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for vercel-ai-gateway provider without model", () => { - expect(getApiProtocol("vercel-ai-gateway")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway)).toBe(OPENAI_API_PROTOCOL) }) }) describe("Opencode Go provider", () => { it("should return 'anthropic' for opencode-go Anthropic-format models (Qwen/MiniMax)", () => { - expect(getApiProtocol("opencode-go", "qwen3.7-max")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "qwen3.7-plus")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "qwen3.6-plus")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m3")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m2.7")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m2.5")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-max")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-plus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.6-plus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m3")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.7")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.5")).toBe(ANTHROPIC_API_PROTOCOL) }) it("should return 'openai' for opencode-go OpenAI-format models (GLM/DeepSeek/etc.)", () => { - expect(getApiProtocol("opencode-go", "glm-5.2")).toBe("openai") - expect(getApiProtocol("opencode-go", "deepseek-v4-pro")).toBe("openai") - expect(getApiProtocol("opencode-go", "kimi-k2.5")).toBe("openai") - expect(getApiProtocol("opencode-go", "mimo-v2.5")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "glm-5.2")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "deepseek-v4-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "kimi-k2.5")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "mimo-v2.5")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for opencode-go without a model", () => { - expect(getApiProtocol("opencode-go")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo)).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for opencode-go with an unknown model id", () => { - expect(getApiProtocol("opencode-go", "some-future-model")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "some-future-model")).toBe(OPENAI_API_PROTOCOL) }) }) describe("Other providers", () => { it("should return 'openai' for non-anthropic providers regardless of model", () => { - expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai") - expect(getApiProtocol("openai", "claude-3-sonnet")).toBe("openai") - expect(getApiProtocol("litellm", "claude-instant")).toBe("openai") - expect(getApiProtocol("ollama", "claude-model")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.openrouter, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.openai, "claude-3-sonnet")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.litellm, "claude-instant")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.ollama, "claude-model")).toBe(OPENAI_API_PROTOCOL) }) }) describe("Edge cases", () => { it("should return 'openai' when provider is undefined", () => { - expect(getApiProtocol(undefined)).toBe("openai") - expect(getApiProtocol(undefined, "claude-3-opus")).toBe("openai") + expect(getApiProtocol(undefined)).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(undefined, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL) }) it("should handle empty strings", () => { - expect(getApiProtocol("vertex", "")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex, "")).toBe(OPENAI_API_PROTOCOL) }) it("should be case-insensitive for claude detection", () => { - expect(getApiProtocol("vertex", "CLAUDE-3-OPUS")).toBe("anthropic") - expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vertex", "ClAuDe-InStAnT")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-3-OPUS")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "ClAuDe-InStAnT")).toBe(ANTHROPIC_API_PROTOCOL) }) }) }) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3898c65bcc..c2286c1dfd 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -29,6 +29,8 @@ import { minimaxModels, mimoModels, isOpencodeGoAnthropicFormatModel, + ANTHROPIC_API_PROTOCOL, + OPENAI_API_PROTOCOL, } from "./providers/index.js" /** @@ -581,25 +583,42 @@ export const modelIdKeysByProvider: Record = { */ // Providers that use Anthropic-style API protocol. -export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"] +export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = [ + providerIdentifiers.anthropic, + providerIdentifiers.bedrock, + providerIdentifiers.minimax, +] + +const ANTHROPIC_MODEL_GATEWAY_PROVIDERS: ProviderName[] = [ + providerIdentifiers.vercelAiGateway, + providerIdentifiers.zooGateway, +] + +const ANTHROPIC_MODEL_ID_PREFIX = "anthropic/" +const CLAUDE_MODEL_ID_FRAGMENT = "claude" export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) { - return "anthropic" + return ANTHROPIC_API_PROTOCOL } - if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) { - return "anthropic" + if ( + provider && + provider === providerIdentifiers.vertex && + modelId && + modelId.toLowerCase().includes(CLAUDE_MODEL_ID_FRAGMENT) + ) { + return ANTHROPIC_API_PROTOCOL } // Vercel AI Gateway and Zoo Gateway use the anthropic protocol for anthropic models. if ( provider && - ["vercel-ai-gateway", "zoo-gateway"].includes(provider) && + ANTHROPIC_MODEL_GATEWAY_PROVIDERS.includes(provider) && modelId && - modelId.toLowerCase().startsWith("anthropic/") + modelId.toLowerCase().startsWith(ANTHROPIC_MODEL_ID_PREFIX) ) { - return "anthropic" + return ANTHROPIC_API_PROTOCOL } // Opencode Go routes a subset of its models (Qwen, MiniMax) through the @@ -609,11 +628,16 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str // models must use the anthropic protocol so token/cost aggregation adds the // cache tokens back into the input total — otherwise the cached prefix is // dropped from `contextTokens`, undercounting context-window usage. - if (provider && provider === "opencode-go" && modelId && isOpencodeGoAnthropicFormatModel(modelId)) { - return "anthropic" + if ( + provider && + provider === providerIdentifiers.opencodeGo && + modelId && + isOpencodeGoAnthropicFormatModel(modelId) + ) { + return ANTHROPIC_API_PROTOCOL } - return "openai" + return OPENAI_API_PROTOCOL } /** diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index f5f7e71116..971b92211a 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -5,6 +5,7 @@ import type { ModelInfo } from "../model.js" export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-5" +export const ANTHROPIC_API_PROTOCOL = "anthropic" export const anthropicModels = { "claude-sonnet-4-6": { diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 1601961964..5f114e41f6 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -3,6 +3,7 @@ import type { ModelInfo } from "../model.js" // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels +export const OPENAI_API_PROTOCOL = "openai" export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.6-sol" export const openAiNativeModels = { diff --git a/src/shared/__tests__/checkExistApiConfig.spec.ts b/src/shared/__tests__/checkExistApiConfig.spec.ts index 5c32e567e2..ab92beea36 100644 --- a/src/shared/__tests__/checkExistApiConfig.spec.ts +++ b/src/shared/__tests__/checkExistApiConfig.spec.ts @@ -1,6 +1,6 @@ // npx vitest run src/shared/__tests__/checkExistApiConfig.spec.ts -import type { ProviderSettings } from "@roo-code/types" +import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { checkExistKey } from "../checkExistApiConfig" @@ -66,16 +66,20 @@ describe("checkExistKey", () => { expect(checkExistKey(config)).toBe(true) }) + it("recognizes keyless providers through their canonical identifiers", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.fakeAi })).toBe(true) + }) + it("should return true for openai-codex provider without API key", () => { const config: ProviderSettings = { - apiProvider: "openai-codex", + apiProvider: providerIdentifiers.openaiCodex, } expect(checkExistKey(config)).toBe(true) }) it("should return true for qwen-code provider without API key", () => { const config: ProviderSettings = { - apiProvider: "qwen-code", + apiProvider: providerIdentifiers.qwenCode, } expect(checkExistKey(config)).toBe(true) }) @@ -95,6 +99,10 @@ describe("checkExistKey", () => { expect(checkExistKey(config)).toBe(true) }) + it("recognizes OAuth authentication through the canonical Kimi Code identifier", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.kimiCode, kimiCodeAuthMethod: "oauth" })).toBe(true) + }) + it("should return true for kimi-code provider without auth method (defaults to OAuth)", () => { const config: ProviderSettings = { apiProvider: "kimi-code", @@ -128,6 +136,10 @@ describe("checkExistKey", () => { expect(checkExistKey(config, false)).toBe(false) }) + it("recognizes session authentication through the canonical Zoo Gateway identifier", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.zooGateway }, true)).toBe(true) + }) + it("should return true for zoo-gateway when profile has zooSessionToken", () => { const config: ProviderSettings = { apiProvider: "zoo-gateway", diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 9ce55a9ac3..3560199b91 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -1,4 +1,4 @@ -import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, ProviderSettings } from "@roo-code/types" +import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, providerIdentifiers, ProviderSettings } from "@roo-code/types" /** * Returns whether a provider profile is sufficiently configured to leave the @@ -14,17 +14,22 @@ export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAut } // Special case for fake-ai, openai-codex, and qwen-code providers which don't need any configuration. - if (config.apiProvider && ["fake-ai", "openai-codex", "qwen-code"].includes(config.apiProvider)) { + const configurationFreeProviders: ProviderSettings["apiProvider"][] = [ + providerIdentifiers.fakeAi, + providerIdentifiers.openaiCodex, + providerIdentifiers.qwenCode, + ] + if (config.apiProvider && configurationFreeProviders.includes(config.apiProvider)) { return true } - if (config.apiProvider === "kimi-code" && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") { + if (config.apiProvider === providerIdentifiers.kimiCode && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") { return true } // Zoo Gateway uses session auth (profile token and/or global Zoo Code login), // not a traditional API key listed in SECRET_STATE_KEYS. - if (config.apiProvider === "zoo-gateway") { + if (config.apiProvider === providerIdentifiers.zooGateway) { return Boolean(config.zooSessionToken) || Boolean(zooCodeIsAuthenticated) } From 569b43df991b5c56ee21cac5514eff36dd40d217 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:20:12 +0300 Subject: [PATCH 79/89] refactor(api): centralize service-tier primitives (#1040) * refactor(api): centralize service-tier primitives * test(openai-native): cover omitted completion tiers * test(openai-native): cover resolved streaming tiers * test(api): cover service tier fallbacks * test(api): cover remaining service tier branches * refactor(api): remove duplicate tier capture * test(openai-native): cover flex service tier * test(openai-native): expand service tier coverage * refactor: address service tier review feedback * refactor(api): address service tier review feedback * test(webview): add model info visual snapshot * fix(webview): isolate visual test dependencies * test(webview): update service tier visual baseline * fix(webview): add @source directive so Tailwind utilities generate in CT builds --------- Co-authored-by: Elliott de Launay --- packages/types/package.json | 8 + packages/types/src/model.ts | 13 +- src/api/providers/__tests__/bedrock.spec.ts | 17 +- .../__tests__/openai-native-usage.spec.ts | 4 +- .../providers/__tests__/openai-native.spec.ts | 270 +++++++++++++++++- src/api/providers/bedrock.ts | 5 +- src/api/providers/openai-native.ts | 42 +-- src/shared/cost.ts | 5 +- src/utils/__tests__/cost.spec.ts | 6 +- webview-ui/playwright-ct.config.ts | 7 +- webview-ui/playwright/TranslationContext.ts | 13 + .../settings/ModelDescriptionMarkdown.tsx | 2 +- .../src/components/settings/ModelInfoView.tsx | 87 +++--- .../settings/__tests__/ModelInfoView.spec.tsx | 129 +++++++++ .../ModelInfoView.visual.fixture.tsx | 68 +++++ .../__tests__/ModelInfoView.visual.tsx | 15 + .../model-info-service-tier-pricing-dark.png | Bin 0 -> 18087 bytes .../components/settings/providers/OpenAI.tsx | 16 +- .../providers/__tests__/OpenAI.spec.tsx | 92 ++++++ .../__screenshots__/zoo-hero-dark.png | Bin 20809 -> 2009 bytes webview-ui/src/index.css | 2 + 21 files changed, 702 insertions(+), 99 deletions(-) create mode 100644 webview-ui/playwright/TranslationContext.ts create mode 100644 webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx create mode 100644 webview-ui/src/components/settings/__tests__/ModelInfoView.visual.tsx create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/model-info-service-tier-pricing-dark.png create mode 100644 webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx diff --git a/packages/types/package.json b/packages/types/package.json index c4cbd4454c..98cf9f373a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -11,6 +11,14 @@ "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } + }, + "./model": { + "types": "./src/model.ts", + "import": "./src/model.ts" + }, + "./provider-identifiers": { + "types": "./src/provider-identifiers.ts", + "import": "./src/provider-identifiers.ts" } }, "scripts": { diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 257a30f3e7..28a32eecb4 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -54,10 +54,19 @@ export const verbosityLevelsSchema = z.enum(verbosityLevels) export type VerbosityLevel = z.infer +/** Serialized service tier field used in provider request payloads and responses. */ +export const SERVICE_TIER_KEY = "service_tier" + /** - * Service tiers (OpenAI Responses API) + * Service tiers for the public OpenAI Responses API. */ -export const serviceTiers = ["default", "flex", "priority"] as const +export const OpenAiServiceTier = { + Default: "default", + Flex: "flex", + Priority: "priority", +} as const + +export const serviceTiers = [OpenAiServiceTier.Default, OpenAiServiceTier.Flex, OpenAiServiceTier.Priority] as const export const serviceTierSchema = z.enum(serviceTiers) export type ServiceTier = z.infer diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 71c2d1cd0c..b025f33f02 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -58,6 +58,7 @@ import { BEDROCK_1M_CONTEXT_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, bedrockModels, + SERVICE_TIER_KEY, ApiProviderError, } from "@roo-code/types" @@ -1233,10 +1234,10 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // service_tier should be at the top level of the payload - expect(commandArg.service_tier).toBe("PRIORITY") + expect(commandArg[SERVICE_TIER_KEY]).toBe("PRIORITY") // service_tier should NOT be in additionalModelRequestFields if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1263,10 +1264,10 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // service_tier should be at the top level of the payload - expect(commandArg.service_tier).toBe("FLEX") + expect(commandArg[SERVICE_TIER_KEY]).toBe("FLEX") // service_tier should NOT be in additionalModelRequestFields if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1294,9 +1295,9 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // Service tier should NOT be included for unsupported models (at top level or in additionalModelRequestFields) - expect(commandArg.service_tier).toBeUndefined() + expect(commandArg[SERVICE_TIER_KEY]).toBeUndefined() if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1323,9 +1324,9 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // Service tier should NOT be included when not specified (at top level or in additionalModelRequestFields) - expect(commandArg.service_tier).toBeUndefined() + expect(commandArg[SERVICE_TIER_KEY]).toBeUndefined() if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) }) diff --git a/src/api/providers/__tests__/openai-native-usage.spec.ts b/src/api/providers/__tests__/openai-native-usage.spec.ts index a266642e7a..184f04ec80 100644 --- a/src/api/providers/__tests__/openai-native-usage.spec.ts +++ b/src/api/providers/__tests__/openai-native-usage.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from "vitest" import { OpenAiNativeHandler } from "../openai-native" -import { openAiNativeModels } from "@roo-code/types" +import { OpenAiServiceTier, openAiNativeModels } from "@roo-code/types" describe("OpenAiNativeHandler - normalizeUsage", () => { let handler: OpenAiNativeHandler @@ -468,7 +468,7 @@ describe("OpenAiNativeHandler - normalizeUsage", () => { it("should not apply GPT-5.4 long-context pricing to priority tier", () => { handler = new OpenAiNativeHandler({ openAiNativeApiKey: "test-key", - openAiNativeServiceTier: "priority", + openAiNativeServiceTier: OpenAiServiceTier.Priority, }) const usage = { diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 1acb4101be..cba0779191 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -13,7 +13,7 @@ vitest.mock("@roo-code/telemetry", () => ({ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { ApiProviderError } from "@roo-code/types" +import { ApiProviderError, OpenAiServiceTier, SERVICE_TIER_KEY, serviceTiers } from "@roo-code/types" import { OpenAiNativeHandler } from "../openai-native" import { ApiHandlerOptions } from "../../../shared/api" @@ -22,6 +22,24 @@ import { Package } from "../../../shared/package" // Mock OpenAI client - now everything uses Responses API const mockResponsesCreate = vitest.fn() +const serviceTierPricingCases = [ + { + requestedTier: OpenAiServiceTier.Default, + resolvedTier: OpenAiServiceTier.Priority, + expectedCost: 0.00275, + }, + { + requestedTier: OpenAiServiceTier.Priority, + resolvedTier: OpenAiServiceTier.Flex, + expectedCost: 0.00055, + }, + { + requestedTier: OpenAiServiceTier.Flex, + resolvedTier: OpenAiServiceTier.Default, + expectedCost: 0.0011, + }, +] + vitest.mock("openai", () => { return { __esModule: true, @@ -122,6 +140,210 @@ describe("OpenAiNativeHandler", () => { }) describe("createMessage", () => { + it.each(serviceTiers)("should include the selected %s service tier", async (serviceTier) => { + mockResponsesCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() {}, + }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: serviceTier, + }) + + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + void chunk + } + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ [SERVICE_TIER_KEY]: serviceTier }), + expect.any(Object), + ) + }) + + it.each(serviceTierPricingCases)( + "prices SDK stream usage using resolved $resolvedTier tier instead of requested $requestedTier tier", + async ({ requestedTier, resolvedTier, expectedCost }) => { + mockResponsesCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + type: "response.done", + response: { + [SERVICE_TIER_KEY]: resolvedTier, + usage: { input_tokens: 100, output_tokens: 20 }, + }, + } + }, + }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: requestedTier, + }) + + const chunks = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual( + expect.objectContaining({ + type: "usage", + inputTokens: 100, + outputTokens: 20, + totalCost: expectedCost, + }), + ) + }, + ) + + it.each([ + { + name: "an explicitly selected default tier", + modelId: "gpt-5.4" as const, + requestedTier: OpenAiServiceTier.Default, + resolvedTier: undefined, + expectedCost: 0.22, + }, + { + name: "no selected service tier", + modelId: "gpt-5.4" as const, + requestedTier: undefined, + resolvedTier: undefined, + expectedCost: 0.22, + }, + { + name: "a resolved service tier without a pricing entry", + modelId: "gpt-5.6-luna" as const, + requestedTier: OpenAiServiceTier.Default, + resolvedTier: OpenAiServiceTier.Priority, + expectedCost: 0.088, + }, + ])("retains standard pricing for $name", async ({ modelId, requestedTier, resolvedTier, expectedCost }) => { + mockResponsesCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + type: "response.done", + response: { + ...(resolvedTier ? { [SERVICE_TIER_KEY]: resolvedTier } : {}), + usage: { + input_tokens: 100_000, + output_tokens: 1_000, + cache_read_input_tokens: 20_000, + }, + }, + } + }, + }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: modelId, + openAiNativeServiceTier: requestedTier, + }) + + const chunks = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((chunk) => chunk.type === "usage") + expect(usageChunk).toBeDefined() + expect(usageChunk?.totalCost).toBeCloseTo(expectedCost, 6) + }) + + it.each(serviceTierPricingCases)( + "requests $requestedTier but prices manual SSE fallback usage using OpenAI's resolved $resolvedTier tier", + async ({ requestedTier, resolvedTier, expectedCost }) => { + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify({ + type: "response.done", + response: { + [SERVICE_TIER_KEY]: resolvedTier, + usage: { input_tokens: 100, output_tokens: 20 }, + }, + })}\n\n`, + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as typeof fetch + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: requestedTier, + }) + + const chunks = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + const [, request] = mockFetch.mock.calls[0] + expect(JSON.parse(request.body)).toMatchObject({ [SERVICE_TIER_KEY]: requestedTier }) + expect(chunks).toContainEqual(expect.objectContaining({ type: "usage", totalCost: expectedCost })) + }, + ) + + it.each(serviceTierPricingCases)( + "captures resolved $resolvedTier tier from a manual SSE completion event when $requestedTier was requested", + async ({ requestedTier, resolvedTier, expectedCost }) => { + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify({ + type: "response.completed", + response: { [SERVICE_TIER_KEY]: resolvedTier }, + })}\n\n`, + ), + ) + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify({ + type: "response.usage", + usage: { input_tokens: 100, output_tokens: 20 }, + })}\n\n`, + ), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as typeof fetch + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: requestedTier, + }) + + const chunks = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual( + expect.objectContaining({ + type: "usage", + inputTokens: 100, + outputTokens: 20, + totalCost: expectedCost, + }), + ) + }, + ) + it("should handle streaming responses via Responses API", async () => { // Mock fetch for Responses API fallback const mockFetch = vitest.fn().mockResolvedValue({ @@ -221,6 +443,50 @@ describe("OpenAiNativeHandler", () => { ) }) + it.each(serviceTiers)("should include the selected %s service tier", async (serviceTier) => { + mockResponsesCreate.mockResolvedValue({ output: [] }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: serviceTier, + }) + + await handler.completePrompt("Test prompt") + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ + stream: false, + [SERVICE_TIER_KEY]: serviceTier, + }), + expect.any(Object), + ) + }) + + it("should omit the service tier when none is configured", async () => { + mockResponsesCreate.mockResolvedValue({ output: [] }) + + await handler.completePrompt("Test prompt") + + const [request] = mockResponsesCreate.mock.calls[0] + expect(request.stream).toBe(false) + expect(request).not.toHaveProperty(SERVICE_TIER_KEY) + }) + + it("should omit a configured service tier that the model does not support", async () => { + mockResponsesCreate.mockResolvedValue({ output: [] }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-luna", + openAiNativeServiceTier: OpenAiServiceTier.Priority, + }) + + await handler.completePrompt("Test prompt") + + const [request] = mockResponsesCreate.mock.calls[0] + expect(request.stream).toBe(false) + expect(request).not.toHaveProperty(SERVICE_TIER_KEY) + }) + it("should handle SDK errors in completePrompt", async () => { // Mock SDK to throw an error mockResponsesCreate.mockRejectedValue(new Error("API Error")) @@ -332,7 +598,7 @@ describe("OpenAiNativeHandler", () => { expect(modelInfo.info.longContextPricing).toBeUndefined() expect(modelInfo.info.tiers).toEqual([ expect.objectContaining({ - name: "flex", + name: OpenAiServiceTier.Flex, outputPrice: 0.625, }), ]) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 645ad8d354..0d39e843c4 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -33,6 +33,7 @@ import { BEDROCK_GLOBAL_INFERENCE_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, BEDROCK_SERVICE_TIER_PRICING, + SERVICE_TIER_KEY, ApiProviderError, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -99,7 +100,7 @@ interface BedrockPayload { // AWS Bedrock service tiers (STANDARD, FLEX, PRIORITY) are specified at the top level // https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html type BedrockPayloadWithServiceTier = BedrockPayload & { - service_tier?: BedrockServiceTier + [SERVICE_TIER_KEY]?: BedrockServiceTier } // Define specific types for content block events to avoid 'as any' usage @@ -553,7 +554,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH ...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }), toolConfig, // Add service_tier as a top-level parameter (not inside additionalModelRequestFields) - ...(useServiceTier && { service_tier: this.options.awsBedrockServiceTier }), + ...(useServiceTier && { [SERVICE_TIER_KEY]: this.options.awsBedrockServiceTier }), } // Create AbortController with 10 minute timeout diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index a1ce2d89d0..8dffe03dcc 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -13,6 +13,8 @@ import { type ReasoningEffort, type VerbosityLevel, type ReasoningEffortExtended, + OpenAiServiceTier, + SERVICE_TIER_KEY, type ServiceTier, ApiProviderError, } from "@roo-code/types" @@ -318,7 +320,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio max_output_tokens?: number store?: boolean instructions?: string - service_tier?: ServiceTier + [SERVICE_TIER_KEY]?: ServiceTier include?: string[] /** Prompt cache retention policy: "in_memory" (default) or "24h" for extended caching */ prompt_cache_retention?: "in_memory" | "24h" @@ -334,8 +336,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } // Validate requested tier against model support; if not supported, omit. - const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined - const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || []) + const serviceTier = this.getAllowedServiceTier(model) // Decide whether to enable extended prompt cache retention for this request const promptCacheRetention = this.getPromptCacheRetention(model) @@ -368,10 +369,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Use the per-request reserved output computed by Roo (params.maxTokens from getModelParams). ...(model.maxTokens ? { max_output_tokens: model.maxTokens } : {}), // Include tier when selected and supported by the model, or when explicitly "default" - ...(requestedTier && - (requestedTier === "default" || allowedTierNames.has(requestedTier)) && { - service_tier: requestedTier, - }), + ...(serviceTier && { [SERVICE_TIER_KEY]: serviceTier }), // Enable extended prompt cache retention for models that support it. // This uses the OpenAI Responses API `prompt_cache_retention` parameter. ...(promptCacheRetention ? { prompt_cache_retention: promptCacheRetention } : {}), @@ -705,8 +703,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio const parsed = JSON.parse(data) // Capture resolved service tier if present - if (parsed.response?.service_tier) { - this.lastServiceTier = parsed.response.service_tier as ServiceTier + if (parsed.response?.[SERVICE_TIER_KEY]) { + this.lastServiceTier = parsed.response[SERVICE_TIER_KEY] as ServiceTier } // Capture complete output array (includes reasoning items with encrypted_content) if (parsed.response?.output && Array.isArray(parsed.response.output)) { @@ -1015,10 +1013,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio ) } } else if (parsed.type === "response.completed" || parsed.type === "response.done") { - // Capture resolved service tier if present - if (parsed.response?.service_tier) { - this.lastServiceTier = parsed.response.service_tier as ServiceTier - } // Capture top-level response id if (parsed.response?.id) { this.lastResponseId = parsed.response.id as string @@ -1146,8 +1140,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio */ private async *processEvent(event: any, model: OpenAiNativeModel): ApiStream { // Capture resolved service tier when available - if (event?.response?.service_tier) { - this.lastServiceTier = event.response.service_tier as ServiceTier + if (event?.response?.[SERVICE_TIER_KEY]) { + this.lastServiceTier = event.response[SERVICE_TIER_KEY] as ServiceTier } // Capture complete output array (includes reasoning items with encrypted_content) if (event?.response?.output && Array.isArray(event.response.output)) { @@ -1418,7 +1412,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio * If no tier or no overrides exist, the original ModelInfo is returned. */ private applyServiceTierPricing(info: ModelInfo, tier?: ServiceTier): ModelInfo { - if (!tier || tier === "default") return info + if (!tier || tier === OpenAiServiceTier.Default) return info // Find the tier with matching name in the tiers array const tierInfo = info.tiers?.find((t) => t.name === tier) @@ -1433,6 +1427,15 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } } + private getAllowedServiceTier(model: OpenAiNativeModel): ServiceTier | undefined { + const requestedTier = this.options.openAiNativeServiceTier + const allowedTierNames = new Set(model.info.tiers?.map(({ name }) => name).filter(Boolean)) + + return requestedTier === OpenAiServiceTier.Default || (requestedTier && allowedTierNames.has(requestedTier)) + ? requestedTier + : undefined + } + // Removed isResponsesApiModel method as ALL models now use the Responses API override getModel() { @@ -1510,10 +1513,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } // Include service tier if selected and supported - const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined - const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || []) - if (requestedTier && (requestedTier === "default" || allowedTierNames.has(requestedTier))) { - requestBody.service_tier = requestedTier + const serviceTier = this.getAllowedServiceTier(model) + if (serviceTier) { + requestBody[SERVICE_TIER_KEY] = serviceTier } // Add reasoning if supported diff --git a/src/shared/cost.ts b/src/shared/cost.ts index 8954904fda..754a2d84df 100644 --- a/src/shared/cost.ts +++ b/src/shared/cost.ts @@ -1,5 +1,4 @@ -import type { ModelInfo } from "@roo-code/types" -import type { ServiceTier } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo, type ServiceTier } from "@roo-code/types" export interface ApiCostResult { totalInputTokens: number @@ -13,7 +12,7 @@ function applyLongContextPricing(modelInfo: ModelInfo, totalInputTokens: number, return modelInfo } - const effectiveServiceTier = serviceTier ?? "default" + const effectiveServiceTier = serviceTier ?? OpenAiServiceTier.Default if (pricing.appliesToServiceTiers && !pricing.appliesToServiceTiers.includes(effectiveServiceTier)) { return modelInfo } diff --git a/src/utils/__tests__/cost.spec.ts b/src/utils/__tests__/cost.spec.ts index 6f0b594c8d..b369a0405b 100644 --- a/src/utils/__tests__/cost.spec.ts +++ b/src/utils/__tests__/cost.spec.ts @@ -1,6 +1,6 @@ // npx vitest utils/__tests__/cost.spec.ts -import type { ModelInfo } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo } from "@roo-code/types" import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../../shared/cost" @@ -283,7 +283,7 @@ describe("Cost Utility", () => { thresholdTokens: 272_000, inputPriceMultiplier: 2, outputPriceMultiplier: 1.5, - appliesToServiceTiers: ["default", "flex"], + appliesToServiceTiers: [OpenAiServiceTier.Default, OpenAiServiceTier.Flex], }, } @@ -293,7 +293,7 @@ describe("Cost Utility", () => { 1_000, undefined, 100_000, - "priority", + OpenAiServiceTier.Priority, ) // Input cost: (5.0 / 1_000_000) * (300000 - 100000) = 1.0 diff --git a/webview-ui/playwright-ct.config.ts b/webview-ui/playwright-ct.config.ts index 8b236781b2..3eb0abac7b 100644 --- a/webview-ui/playwright-ct.config.ts +++ b/webview-ui/playwright-ct.config.ts @@ -58,10 +58,15 @@ export default defineConfig({ ], resolve: { alias: { + "@src/i18n/TranslationContext": path.resolve(dirname, "./playwright/TranslationContext.ts"), "@": path.resolve(dirname, "./src"), "@src": path.resolve(dirname, "./src"), "@roo": path.resolve(dirname, "../src/shared"), - vscode: path.resolve(dirname, "./src/__mocks__/vscode.ts"), + "@vscode/webview-ui-toolkit/react": path.resolve( + dirname, + "./src/__mocks__/@vscode/webview-ui-toolkit/react.tsx", + ), + vscode: path.resolve(dirname, "../src/__mocks__/vscode.js"), }, }, define: { diff --git a/webview-ui/playwright/TranslationContext.ts b/webview-ui/playwright/TranslationContext.ts new file mode 100644 index 0000000000..b2024e29d9 --- /dev/null +++ b/webview-ui/playwright/TranslationContext.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react" + +type TranslationContextValue = { + t: (key: string, options?: Record) => string + i18n: unknown +} + +export const TranslationContext = createContext({ + t: (key: string) => key, + i18n: null, +}) + +export const useAppTranslation = () => useContext(TranslationContext) diff --git a/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx b/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx index b04ab1163e..b00a36d1ac 100644 --- a/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx +++ b/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx @@ -3,7 +3,7 @@ import { memo, useEffect, useRef, useState } from "react" import { useRemark } from "react-remark" import { cn } from "@/lib/utils" -import { Collapsible, CollapsibleTrigger } from "@/components/ui" +import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible" import { StyledMarkdown } from "./styles" diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index e043f68f83..fff55eda55 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -1,6 +1,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import type { ModelInfo } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo, type ServiceTier } from "@roo-code/types/model" +import { providerIdentifiers } from "@roo-code/types/provider-identifiers" import { formatPrice } from "@src/utils/formatPrice" import { cn } from "@src/lib/utils" @@ -17,6 +18,26 @@ type ModelInfoViewProps = { hidePricing?: boolean } +type TierPricingRowProps = { + tier: ServiceTier + label: string + modelInfo?: ModelInfo +} + +const TierPricingRow = ({ tier, label, modelInfo }: TierPricingRowProps) => { + const tierInfo = modelInfo?.tiers?.find(({ name }) => name === tier) + const fmt = (price?: number) => (typeof price === "number" ? formatPrice(price) : "—") + + return ( + + {label} + {fmt(tierInfo?.inputPrice ?? modelInfo?.inputPrice)} + {fmt(tierInfo?.outputPrice ?? modelInfo?.outputPrice)} + {fmt(tierInfo?.cacheReadsPrice ?? modelInfo?.cacheReadsPrice)} + + ) +} + export const ModelInfoView = ({ apiProvider, selectedModelId, @@ -29,9 +50,11 @@ export const ModelInfoView = ({ // Show tiered pricing table for OpenAI Native when model supports non-standard tiers const allowedTierNames = - modelInfo?.tiers?.filter((t) => t.name === "flex" || t.name === "priority")?.map((t) => t.name) ?? [] - const shouldShowTierPricingTable = apiProvider === "openai-native" && allowedTierNames.length > 0 - const fmt = (n?: number) => (typeof n === "number" ? `${formatPrice(n)}` : "—") + modelInfo?.tiers + ?.filter((t) => t.name === OpenAiServiceTier.Flex || t.name === OpenAiServiceTier.Priority) + ?.map((t) => t.name) ?? [] + const shouldShowTierPricingTable = apiProvider === providerIdentifiers.openaiNative && allowedTierNames.length > 0 + const fmt = (n?: number) => (typeof n === "number" ? formatPrice(n) : "—") const baseInfoItems = [ typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && ( @@ -144,51 +167,19 @@ export const ModelInfoView = ({ {fmt(modelInfo?.outputPrice)} {fmt(modelInfo?.cacheReadsPrice)} - {allowedTierNames.includes("flex") && ( - - {t("settings:serviceTier.flex")} - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.inputPrice ?? - modelInfo?.inputPrice, - )} - - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.outputPrice ?? - modelInfo?.outputPrice, - )} - - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.cacheReadsPrice ?? - modelInfo?.cacheReadsPrice, - )} - - + {allowedTierNames.includes(OpenAiServiceTier.Flex) && ( + )} - {allowedTierNames.includes("priority") && ( - - {t("settings:serviceTier.priority")} - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.inputPrice ?? - modelInfo?.inputPrice, - )} - - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.outputPrice ?? - modelInfo?.outputPrice, - )} - - - {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.cacheReadsPrice ?? - modelInfo?.cacheReadsPrice, - )} - - + {allowedTierNames.includes(OpenAiServiceTier.Priority) && ( + )} diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx new file mode 100644 index 0000000000..938bc8bff7 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx @@ -0,0 +1,129 @@ +import { OpenAiServiceTier, providerIdentifiers, type ModelInfo } from "@roo-code/types" + +import { render, screen, within } from "@/utils/test-utils" + +import { ModelInfoView } from "../ModelInfoView" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => + ({ + "settings:serviceTier.pricingTableTitle": "Service tier pricing", + "settings:serviceTier.columns.tier": "Tier", + "settings:serviceTier.columns.input": "Input", + "settings:serviceTier.columns.output": "Output", + "settings:serviceTier.columns.cacheReads": "Cache reads", + "settings:serviceTier.standard": "Standard", + "settings:serviceTier.flex": "Flex", + "settings:serviceTier.priority": "Priority", + })[key] ?? key, + }), +})) + +const baseModelInfo: ModelInfo = { + contextWindow: 128_000, + supportsPromptCache: true, + inputPrice: 10, + outputPrice: 20, + cacheReadsPrice: 3, +} + +const defaultProps = { + selectedModelId: "gpt-test", + isDescriptionExpanded: false, + setIsDescriptionExpanded: vi.fn(), +} + +const getPricingRowValues = (tier: string) => { + const row = screen.getByRole("cell", { name: tier }).closest("tr") + expect(row).not.toBeNull() + return within(row!) + .getAllByRole("cell") + .map((cell) => cell.textContent) +} + +describe("ModelInfoView service tier pricing", () => { + it("shows OpenAI Native tier prices with per-field fallback to Standard pricing", () => { + const modelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [ + { name: OpenAiServiceTier.Default, contextWindow: 128_000 }, + { + name: OpenAiServiceTier.Flex, + contextWindow: 128_000, + inputPrice: 4, + cacheReadsPrice: 1, + }, + { + name: OpenAiServiceTier.Priority, + contextWindow: 128_000, + outputPrice: 40, + }, + ], + } + + render() + + expect(screen.getByText("Service tier pricing")).toBeInTheDocument() + expect(getPricingRowValues("Standard")).toEqual(["Standard", "$10.00", "$20.00", "$3.00"]) + expect(getPricingRowValues("Flex")).toEqual(["Flex", "$4.00", "$20.00", "$1.00"]) + expect(getPricingRowValues("Priority")).toEqual(["Priority", "$10.00", "$40.00", "$3.00"]) + }) + + it("only shows the tier pricing table for OpenAI Native models with a non-standard tier", () => { + const tieredModelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [{ name: OpenAiServiceTier.Flex, contextWindow: 128_000 }], + } + const { rerender } = render( + , + ) + + expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument() + + rerender( + , + ) + + expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument() + }) + + it("does not show the tier pricing table when model info has no tiers", () => { + render( + , + ) + + expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument() + }) + + it("shows unavailable prices when neither the tier nor Standard defines them", () => { + render( + , + ) + + expect(getPricingRowValues("Flex")).toEqual(["Flex", "—", "—", "—"]) + expect(getPricingRowValues("Priority")).toEqual(["Priority", "—", "—", "—"]) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx new file mode 100644 index 0000000000..b007d4be69 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx @@ -0,0 +1,68 @@ +import React from "react" + +import { OpenAiServiceTier, type ModelInfo } from "@roo-code/types/model" +import { providerIdentifiers } from "@roo-code/types/provider-identifiers" + +import { TranslationContext } from "@src/i18n/TranslationContext" +import { ModelInfoView } from "../ModelInfoView" + +const modelInfo: ModelInfo = { + contextWindow: 400_000, + maxTokens: 128_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2, + outputPrice: 8, + cacheReadsPrice: 0.5, + tiers: [ + { + name: OpenAiServiceTier.Flex, + contextWindow: 400_000, + inputPrice: 1, + outputPrice: 4, + cacheReadsPrice: 0.25, + }, + { + name: OpenAiServiceTier.Priority, + contextWindow: 400_000, + inputPrice: 3.5, + outputPrice: 14, + cacheReadsPrice: 0.875, + }, + ], +} + +const translations: Record = { + "settings:modelInfo.contextWindow": "Context window:", + "settings:modelInfo.maxOutput": "Max output", + "settings:modelInfo.supportsImages": "Supports images", + "settings:modelInfo.noImages": "Does not support images", + "settings:modelInfo.supportsPromptCache": "Supports prompt caching", + "settings:modelInfo.noPromptCache": "Does not support prompt caching", + "settings:serviceTier.pricingTableTitle": "Service tier pricing (per 1M tokens)", + "settings:serviceTier.columns.tier": "Tier", + "settings:serviceTier.columns.input": "Input", + "settings:serviceTier.columns.output": "Output", + "settings:serviceTier.columns.cacheReads": "Cache reads", + "settings:serviceTier.standard": "Standard", + "settings:serviceTier.flex": "Flex", + "settings:serviceTier.priority": "Priority", +} + +export const ModelInfoViewFixture = () => ( + translations[key] ?? key, + i18n: null as unknown as typeof import("../../../i18n/setup").default, + }}> +
+ {}} + /> +
+
+) diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.tsx new file mode 100644 index 0000000000..0b5074e761 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.tsx @@ -0,0 +1,15 @@ +import React from "react" + +import { expect, test } from "../../../../playwright/coverage-fixture" +import { ModelInfoViewFixture } from "./ModelInfoView.visual.fixture" + +test("renders OpenAI service tier pricing in the VS Code dark theme", async ({ mount }) => { + const component = await mount() + + await component.evaluate(async () => { + await document.fonts.ready + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + }) + + await expect(component).toHaveScreenshot("model-info-service-tier-pricing-dark.png") +}) diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/model-info-service-tier-pricing-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/model-info-service-tier-pricing-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3bee53510b9ab9282bd0e1c910a2b2e58f0a66 GIT binary patch literal 18087 zcmb@uWl&sgwS$>H6)U+y(^&bKGpku7O(fsnu<5Hf8!t9@yvQ!F5N>* zzCiu@4E+HS3JMPXBkubSjL&RPP-IlFW>8QP3Mk`HP%+uxvp+z67Zm*Y3F@c+|J4t| ze_F<*w+ZJdO5TU|UWGiLeu&tqW^CPTVyzGT@mflo?$N|emLCzDuso0=md`%l1;kGpLc<_nzcjG z(zbO+HCgFt`vD4h`0x|AW}|pz@>EhZYkRL?m()rw18pgdu<0U$dsB!0juNC3TIz5Q_-nTg%5( z&1ZZ9evkoJ7Ip?6WHn4;KSB|rl_KpM$LdHudTKoZ)BITDg|WZNeoBXxGgT9Vh?OQ? z+LJPFHh?3&No)6EV4RGZ`ib3P8=jB(#CURgttCH{{Qe05aA=iGV1&7Swh8h4BC{)< zkgZa=b%WE4PrMop-)49zaEwFF!Yr>Mnuny^I?+Gq)?6ZF#oBrTyH&p-NH8|JxF{e6 zYkxU$c6a$&#x;F2_Q~I&jd!z1vak*3Vw0SwA}7*|-p228Um+A8wWg)bEdVCgqn*Og zs%>4klMm*G#kUXa8cb5mLsODJ+Th`~qiU#axm3V2Dm);RsC9f$C8ENzo2BEY^3yX? zx6rDh=|nJ5D4jVnzjggrn)%;Rw-Z7wN_4xBUG!ASq)Y0SAN_0(0dhR#$>G)hA5k|> z>{*Y%*Yj-_twF^dfol3!`jksWeT=3&-Gp6K2*%Nd zY|ei)WGRG7BoT$(J28ob4{Q@lecugQULE*ueQXyZh~%~;ymGDh#db>fNS1Ay%_l^ z`QUAw+W+lXN6D~@!%wTD3U72hn443aV#iLy3>SAFidVsMKcMiyL|_+|%((mHmUX1; z&l~|+_-EcE?1r?qQMUlJy&;{lVZ#d%L%*NdJXFA>kSjm*ZAT4KQmlB{%DK`2pfL8- zO|vl{t00Jz1pUgrHs$N0b*)JEp1ZqYrvOUpN@o|nVMEA|FcCX82RD2y>dicTP}TxU ztKe&>)dHf=rt9fC$V|>Wc zFj-$yem9T`G(xJJm>AYdSTy2ufXkNk!EIeFjhr<{pGion`x!DZ@o=l+AQt`W_RARu zg7;lPP-_yoYbEURdbfekT@#S1fvA|=Fi_p%R^+o%n8fhb=Mye(Xasdn(9Nn*z_LoL z2wmZigq5wZ{_t+Y%@{f8D0?GO}0+>sh0J3B}2?P!ru zSmO7uGTj?`lNkJ}BWDOB>GdhI)TjKw4IDduIbCE?ShTUf78B#GS*qpHA_gr(`l)5ew3F)42k$*>U}$JV=GR#hrhjN;Ppa4P`8c;AP=pXv|$ zjtj-<_~hhb)iSzFUI#XJG1T^HRnS;=U6t{fPl;|PsqVgZG8xf?RBl@ zwPNJd6;GbixyASF8r={aI`p|wL`+F;tj?0qG?X#kc>Tt%hlSa%k}4&Hr8;Oq2P^fO zJ0M?X&`P8FC(X|N$?B0H0}juHP=Jo|OyYRDJFsdUz);!ED1mc1nSBaS!GP zON`GN9W%L$;;ux0^@83gxTq6swX#1+#M&ieR5mO-Ch)wq+6nTCMgR~btuHIRqtqmB zGgqg&thGeB;*q5rejJ(k+ph(Eh!=f0l`^tk-W03rkl7%RWec*CUQi=WWgA*KapVpl z;1feAo*;W8K)xtiTmQ zhL-F)skLdt{?*}H&=4N%+O)_~Nw2NCKDO+sgQMEd1edos^fE)v2gXC##l`Y@3{%+5 zZM=dJ_J29Mh_|N3ZyFt!x0o&b`i*Dg<2=f zhW-~v>fMC;-xDUa7koC_t%FZ>+>;Bl*SF8fba6oiFvwUFNrkM~Vskr7z_Mw2C6;}B zu+G8c*_GU{`}I3+=mQ%|4^t|Bh={}ORD;;THBw6C3*=z?n7|^+AIBsMr^)f}vZK3G zhXTe>WJmj9&SQK{LwM}o;XVtg&YzU3x9(5DE`4Mlp?E81C;baaiOFJEjYPZ25w_6? z335oyGX6+}33aqe8~3m>eVUIr<#74z8c{DJc;7Z%Ugf8;0H2Y1#-91G!#MV>%_~tX zvqLKJw<-zfLdMb2vGfO(S}A!kf?5n8=a)A|-vwiMsk&79Wo_Y`x69=iaA}XM}usJgi(r_ z{521ko6Psrc{!fOkdgjYkZ4ith8U%d!J=m*pSrOBWPnNF!?5S@_X%xZx1T$2$!obQ zxW(V#O7?xd1!^s`=r)Voz2eA(-kIG1DjI8L)1g^Ul&jidG)>#E`{-ah`>&Vwhn=S( zu3c7J#(OE}c_WsusQ!E&aJHxM;0Z`QYdtJ$2|IlA3o0`Roya+UJ)U>gEVS#{u>-~F zK9X3M<%=(c){MeS+53Z{#z0b_Y9b<@9A7kUfiWE8T0^?82jOGWum000MCpRZ^Z3&0 z74@az@<-#3(+05Y3#rrS<{k0+7|W%7m!8li7e2{0hieLcJX7!&DCFWg?-`!WDo;!n z>a|&(LG+>fgcUK`O`{!j{7!*!1IRH#P4ylAZ-uZGTRi~Vm$&{C+DDaQFj5zj1rqn2|W(UTjTxye<9!J&butxjSJZO|G zgqs`oaI_*C3rx1!(Ch7^4gB?G-Jq`iS8qlc2S|q$eclWb z`E`*uK?L=!$7!4hZHRr0+AI|%CLK0JKj%D``QBENqUYnHWh6*~`NZ|nygWswe#LYI zwS8&mYYxtBOMpbs6Ep8!x{rs$bckB8`HT8*Nl$f{tJAq@ukzHhV=iw4s?L?%;z_fs z7r>ouWJLj&y{FF4&rhuvK`?yp?22r?5uBVYQHS{)+EkZC%~tV~Zd3a6;~QXPxQloI z#kc8>c-*FiFMmeuyp^zF1?c!!8~%e^Xu0#B{Lri>WA%O%(B9dVSPa>#`6ZStIDFrn zb+cYyEaxX9IP00;=)BTqD^&Zf!7Sg*@I+Xk>K{2!9Of5Y?}oqWgmD$?`#sa!pmGXy zJ`m7|IWA-AHu3FwWBf4qu$|Jwg*ZJX1gkXMffsvoKIEC=Q zT7Wf%K8ZTq2}ae&F%9*R3(8HS{DHZ}ynO`+6?=e7w}q4UjsXcP@7MQShLW8+)GK>?AgP-{$T78ZvkKsS;dp6WB3 z$F4s0z6$YB%B;c5+w14OD9V1`hj`SzbyGhS#!3m*d=-fwS>D$n*F`Sjke)&6MKmTn znD%d1(Wb?>yP_+nRR;pS$?SZuAxGP;t|v+~J1i6E6@)b>5oyYEMG^-Ln6C=;<}@N? zQND&xyxs2A`%j{4vhA-=#a5EIQfNc%?b5^>Zf`4AH!F>Vx$UG&1vHBV!&@?csNeV%RV)d#VYSrtEd)K4IISfMP)lI*Qoo$Ka^?v{ z+98JWKVv=3|cFwzruSt zxHi{#zT7E^FSTk|YL48C$|1WBn6a@B zP-p|(pPd|B3^ar2j8E+ta6bFf&X|!A8yziO*m~GCJZDSg!Pl1?vGn%o+&-@4O}P?* z4F4_a-w(Gp=0Wk$rm+)CzFBz$Hquba0Xf>Yp z&0pc?!CYSa17VHyfI?}(*USLS+-#x^kQhaD5(0{@sI!7vWn z95ymxyt_blzIvuVw_XvfgM!c4a6OXW7}!{0FMsLisZ!oH^Q~sjS=S%~S}c#i-{EY< zkCx_OHweAKVuwDop|D7dEaQv37?LB5QVrf8?5kGf#s zm|NK0iV$2l!ay1I(V-?MNA{oGshM`97!^`m5tLbAVC}Mq$EO0nAMfT zHNXq`m2GulkWSg=!q{UPJdYmnnZ#bz8nuSYUz{X7{j{nH6Bhuzwq?I`!X(5BrQb7) zHqPspD&(N-n(9a9hhzOa(hPbJ$J=;9Bp zREz5oBZCnE$){IJwiOzA0>9_1GQ3ylXocx8QF8P{b$z~?A?cBjXbyiHAOUjierN4x zrA{H>1b1P_qx#xCFS3wIG3snRrwsj%s^&X2*16hEwVFTW`h=jb9jSFD?gwu6#vC7-_DT7;-T-A#ALUOOA zO>{k6Hv|X{tuK+$+d+y$L*qmkQ9pazj6RPk?#fpzy>{06sP}ed);=;l!-*@38lhcQ zQlf}XwO_QWw;5uD2h)tdW*c$;Br`gI!+%vFkePc-jtI&YmaAFd;R_oE`iTKUOS>i1bexeU%Hc;}j2ir94Ct4I)Vx zu|g0cf`QT(g-n{cG@j_>pt{QvV2eRcBW|akrP?~DfcxzP`E(nZ0VcdxV9^(twe;;s zdNiMI?-|aKf2=zhw(;9h)%{i|C@*Wsq%!_S4uUOjuRO|BDCvxfSsjKA9*lj!D&oOc zsC(`Xw|^PG|C=)WZ`(LLF*P+D;!+k#;0vh?y6Z;^N4 z71t|T27IU(4gJ5bF#e)a81Oz>p2iLY^+uBuugFNBq1?9L@#5mcQys5B&qlmklWs&+ z>DrKcvlOE^#^$(-Q6CijjM^qUo2Np}<#dvA zHHPmg*nx;CBQAxb5~2&z7IA$!0WOZ}V<^NKFIufpMJDw${T-zyyJi zNJ1i_*a1ro3r5>3y@dMp^y|aZR_W%&?~2(5ZzmT6)kLk02UDJ!l9=+k5#}(K%nBt0 zUVvENauH@wlYn1O;j>lKqCwKWH2;W?OM}wH%+r{>?yb?ms*Ev7D1%w9qSh!EXwH&ZC*lhU|mbaXt9x( zY^*gyy_!p;$ZOow%{HO~Wr7<*NN|E4OGv_|@DhqKyMG=rG5|;&q>vB8P~J5roBm&) zFS72-g1dRyedtP|_xL=lZtZ*siON|clpnmkw9|^3GaB%EugZR1W02P(mZ)cWNF*2LOtr}Qr6cj;e zFU5`0cMa6zJ?;e@$sD-1gtkM^+FouV)h$W}zdMrOkwEJtsEnIowkwfq2XIoed*mHZ zznrU*invlfJR$GjS#7L7l4)va6sYGwJc}H?{=QBdv+-!X1-Y)8oj6<>!z*5PO3pg| zePsoB6OyT^6Ff&48eG0$OHb`*T4rNEx6YpEeAoO4fe9nVy_^dUr)_!C z_3^vz)R`5VPzDyP+&~12UQYePsoz6Lzum|vjX9lPR?+eMrxFMfHXQw}v&SmwC$ZdD z51AKgOTv@c$Rs~MMS91xI@H3m=5f@>=9iaHceXL;hKG-jcYayl`0(krFKbgFr1`7f z+>{U8jo}A1i)fWV-qF}|8H-nBTI18`=%M(vW6}_)Uy1%7o`|ejLrIkmoKhuXa;YkN zj6gB-cGRAPW>xLUlC@b*C4q{z*Z65c(F4IfgCg<}K!# zNl3u71XeHgY{6&BX8Wh<1#?Wvg}*ubq*4^pY#MTGYGO5GrfNtS;x1g@V+@gs4SJ-Z zCLkG~pBc}xiu#7Q0x>VVasAnM;GN~O`{bm46!aZjIaA!j%Pr*oWtcBn9L^v|j1>DE z7UObpxbXEMIsFgI_IsTbIv)~RL7K=U$gHoIMtwz!DhzcW({R?ODcs;RCD) z*POX^oQSviBTbTJxpyV?%+H+dVXmegd;3mgNm~Usth5%@HUCqjKd)dZgVYszTSs^3 zLD#+&al!td2aI%Ox%628kYJ?-i=kR279nWrRI-=e+0)pELTdFg6mcst*OWv+S!Ij} zl|Pvz(#h!olP-j#3(#uWowRq2s~fs2Vg&#s@2=vro*Y>|v7;UT_znTEaz(YF`hBIm zo?(g~^AGK7|FemOA*Vj~QzUnyxS&zRY;eJdjc0ewzNb@LHz$|gdPUb^A}O1Gj9}c( zpO|nUY|P}4K=4P8R$ana7pBz0JAem^onTd#NesH)f`o)Vo&PH1&Ni)Bpto_Kk$;s> zTspb%T;W)nv79PXDQ*sB9iNRvCvz4zNfg^XyJk@&L}p4*POXf+jQ8dziwTFd9V5zB z0xDkga+%v&HakVtn1IA69{Gr8CHxCn&mu?35bpNtEZPkiUeC2Wj~$S#4-bpFFYcf> zRg{Lzr7f@e*=So7^a_z)Yj$RF&l*$r&doe6Ye)kbr)Z~H_OvsX;tNix46zdF=cq#Y zh2u%Phx)3>zUA3qB8On(m{}9#Lc2L)keDLpC?Pxpiq`a94)rm8H>7LhAJF(O@1RXn zTxZaDb^F~4y+3cfP$^3<_bN;*ncOViS8cba9T|-UKca`4}3VI4tM6zU z(E1!+LNZW<8m-lfaXRi)&7{}6^+$p0_)dWr{EL$b-r5X`FowPLl7@GB<=OR<5NZ}< zOg|fpswiJ@gFhN71XketBvZt8tWzjiYvT^~^=EL|H4h&wXJ1?Z>|RY#pFq^+h0wVu z*~taVSw9joz|Ax?@uK#>@LU;VrOad!ngZu7%P#_Llwpt))x^fp|(GZ zi@i;01@oU?KxerjT7^j%KG$t!p}^6V6fxMGlph>P&nFn%t2=OzSMbo&=m7K&mt;%JZ|Z@p zc0w>FEcU^iWSD{JKq)eSPxAuF4HG3?Yx`=bPPfWu!NEF{`}DvkU7!Yz|4uy_np5+I z4lC+%M(0Kq<#K(t4B#TY`=ox!T78R;(uvC7%?s)Dn-J757{iYy1Y z|AT&d1;I}9et6>!4K2~?uVJOK?=s%@6JjWU%gk)MJ7@gI;yLwme-S#9JtGK+!+T!-1B($XY!RLI0^THe%x zrvw=@5gNR<&R~2iR5~x8NZ(znh)q!VG4*O{^SL2W-zR&X8zCS0P)gJbjf*|P@Vjl( zKd03__>e$NvIq=VE@EFh^TEIXH$_9#5KSeY1IL~p1Fo8Asc_H%Y&Q86tapcGGN7l< zlZZew|MZv8?{1CYRNfXf5%)^jRmZv02r<;km9cngJAO~0YX|gh1XID->U)w8sMiWy z8p5j|9Fr%+Fw8L*8rM(T$^X$&;SDHr!3=G4CbwuGKPV61vp{p!lEOlrXO}fY6F@8u zH_fM;KZ)WaL4Jz)q!o8@@JA2xeT-6L?D7WJ&KcIqlCE|(0iSI+bJQk)td2K)4U=HW zmIadWbqZXNxWXqu(yuW~-0|PEx|2+Ih3P<}{o9>4r+)bhCrl4x+?0%m)RE=Bxr7by zkMx+9KDW6Fe~Ca+Mo-BR(*1R)D>cj2O*o*KHQ%R?ty-9?M3n_YJhC*W)n(@LSv{3g z1iy2HeQcR9aLp81 zG_ase-kR;WzxWTW86vxlcCA=FrO}6rxWSc^*Or&4s9e|9TElyUA0wp1dha__!-v&; zBCPqe{z0otwzCg6%8kVuj0GKlu~VP2;(DDlcK>Dgl%?))z!y139G+IVFjn+2Qcl7Y ziZEZi?VlzSGX1xRPyeeV&iftRLYe7C!o!m`0wjG;^U$J4^oWUzw?AJFsgBegVzJ5X z3B00;S;Ij6QvF*M)`ea^S)x5Y?(&}XnURu`5-Nr9{;vjmj;_QKqb9LmOVq$Vdm+rO zdrL-RD%3WRr-=6U^#D#0N(w9?@?F##3PAQiYnLEvLQoXui_TpQ6N`_=?y7VFt(2VQsT5Z+>E%qL%KJlKrjhnmEyO z?~hCdRjf0w-M$69i33iUzT}mHx)YYzI`6R>t=pC?A2x@ps>hn|fn(2&yP9yG&r_>% z%KCYqlPh=0iJz_+xmQ3-;6vTngN#7VPo$$z{6J2s15?8}?^F4VVNIm0Yad?MNxT)Z znIwVI1bEJgty9qIi|pQdqe4iU?f&4pTawdM;+z~FQ45yn$A$0S8%1@{oC3`b%B`~1 zs8QS_wlBN>^8H@lo~GSdwMpc}^VDT*ArF4GTPMJtktT}XG>ZDuhm$mv$>(d0TRZ_@ z>+=muQS^hQ%mr|in5Q1kiyALeS%7-)(W_L=3;1Lq^Jwwys1I$-*5`FGWD5^o_tA29 z@6E@2=`A;FZHJS`*21?n&fmh@TjZuaQFXP1CxT`aR=3 zyi9q4eXpZ{*tGlR-_PRBA1{K__+9n8mbtJmdfQ!+$tcsJD4+NJC!9>YyyYs_WQf>e zAvMp?vZIX$*B9;XTZ6$FV*+~VqD;2NCZ^JkX0!~mQ2AG!w81Dm(xLnwS9NPTP2e^_ z4a^&Ax4^Jm`@;sFtgRPQCIp~ffx+M!-0r?J;`<)Z>NkCTxCmj+V0Pg?o&P!KJv#C5 z=>quo1PA0hstJ7jW8J}^*VNSMp9<%5-YekkL~!@|vTOUg%PeJTCG0Uvfq~a!!i{a^ zv_2R@?xH?^m6J_h(2xC7=eP_f?m8Bhr=pu^LqyyxW zetNsEjlK)=I&yY+0eE9Y#kh7 z;N3;7i7W+jeXAI*x8gLGJZHZfd7vQbh<-aMvj-7n_-oO6P2F%WCeg>{`=W%dVH8cw}Fl^^z=oc7_`#zFF@HlOC+QT@E_l%27 zt=9@LMx7^nSiOZk9z6gL<(wYR;l*>%o19@37%kQ1X%shbf zHfrf+Y3j_io}QN0YX5K}+u_ygl&DS5DWjKhZ^y3sJxfd9>&WV#O?!RLzQr8`ou4m0 z3o->s*3>l6lBbdq3L8OF~+M0 zq4nPWw58qd?ut^$tlfxOj{NV2pvKJgz}o7A-uCmK(lKtYE$P;-=V!y97?&IG`!G+g zdyztsV($uLykrMmwwFFTH|5nk$Q{|!-7K;IPKekw=O&zk_QhSd)!MI~uuGt2iS7Lt zOUUdOo9BT$j5MRI>v#b--4^eFw>(#F-=lQcHCRV-hlkNkQhID`BN2GahNYJir42klmd>ASrTuzvnQ7)OIPg~GEL=gkGo4mkGY``{;*f$C%K2a zOhqrA{cCHCB60lL4>}50E%N z*C&((@+S#H4YniEh`#z}-h1&?l810%UPY}M)S30KvvrcqfXw!Ri_?}0TNB%-Ro@ps zpegX4bSv$Rg-ddmkxxY6TjS*u6Sd;0w@W22AMfSHLqwL$GR-7qKQ!Fc&d5ZpDC%{V zZ_4wfd_C><8ttGd;PnIwK|P80>TZR?b#S)`HE;odljwkBw8s2PEQvpy(k|^W)#6f-?m9h;^Fx_4bengmFIeHI&n6r20?5`eaK1_#Ip$5U2hF<$rw;m; zkzpWSH*n&5^bt12>D6a9K5%=RNHPon`#SksCVmNc93#*$$6llkrj@sCbl7w)TvZgC z(3lZwm+`DBfZ3T*fv=cX04tkgrydFSog9lql)2Twwwk7$O|6gCMX~&*D@U&DH`)l1&JkQdtu7kJ_Lt^!{w{$x^{K(Wfh}Y zX2k*%c%$?J9&YV6<2TJluL?2)jFqc+JnFu`lSv|w3B0Qu8asX#m(T_U6d}7dE9gE< zG+QJdP4|e?xhFCHE|UVJ7HsUh>qV*%@+$&P?b7R&936QJp%Y`~g?1oq%Mq0FVnehw zbXceS-c5rO+eGbWt}W^-DN1G^M2ot@Pq}Rt6iZdEzC~A$Z(;M@pVY znB&J}HcDt|(hduuVTOZ?&#bfz>2bm{bC@b2`?DoKYt^e&bA*=dlyAbBoXdLviz$Hv>=f?dqCjvu}4?V)!Hxojxeuqj6f5RF;n zslOuQ0)}z8n9ME?q>P2~OaW*fZ6CRJYtn0eM?)qx{){p2VOqgzFi8CWY~jMS1}DAs0OPcRh!O0(={CYtdsM5-7?)?aS=AF`b5oi171jG?wi4N6(11&AB_laG z4V3!1 zKpauC*pLAk36keiCtqw2o$Vdy7B6XM3ur0v&p(5RShLp3{m<$BPaR$6dLkGSRDXGv zSl-G$8?E17A6czrZtBN_!PI8MJAZC*5sN@GoQQ?RQYOU>K3T;j#S25Hwm+O7og(Z7 zTfT~7)0+;&DPP{s99(&4M7fRDV0%|J;TURMGrTY4FS}T^VhVg@6Sq7*kGTP@vz0N7^Je-GrJRpCxz4OuT)|vS#X~++17K_Ep^qd#`n_rt4(! zf!A9Yp!QUy>|!J7jCl1dopKp8z*|v}vq4f^I#C*UPYT-c%KoY6&+vNZy8}5VomN^K z)`=^EO&*?v11+pzSNvHNo+(*gxB|E}wp?^CO$@&6rG%d#4%eR=8q75*k3_e2QPLC_ z=G|Agz>l8c0tm717@Hz1IWtT%3DepGB61-Wf@ZTfV>2Han%oz-=C-XP!*l0g#CO z=BA}1)m32di)>FJ!uWuu? zieU~DZP;bg<5=A{4{zoK5{6=am2$&&-{ZtNKtxH@@Z5~bkpuW{0vRt2FRG&WUi-w7 z2BA7mfo*(k)okDUTGzun*+2Cq@lv5{gZC+{tH@dz0qD;APDM7F$JLXzM`l;g^&2*h z-RGuRFl+6iFD<+!b#awdU5uoM+~wn{PrEmS*wSp62kk4WOfn8_+dkq(1OZME5yjU0 zPe$nkzYA75O4F)NjG`Cyzi?0UrD(TRw*H;q)ltwc|JtIXvO4)g>D<1B)1~57f$<$$ zxE~F7>;n9hRMp{Ove8%jCP}MjA{VEDFgHFg(hnn}wB-z88J2E4JIQ)B zOu3V(gU`Rq+e~8U=8|coOP%xU3Rc93&%gKR^kN|_s{0!I3TD1{NVr@Bd;jd1{-y4T zR#M@51)XDHniLh}$C1Iy2W~lbpCiM*QD=(}Uqbg_C8?B9jN;Hqr*(iV-w`V1%sU{K z=7*A=c8Yp74GL>fj|mNx&tpcGiG@{U_(-PT5Gw%t0*5OsEiRbRD|Ix}((`17bm!s7 z)dA4QfW$#vYGYNI`DVKQvK6vqIwu*9;NTck*LMYW(M02nI2w{Q;=wXXx$kd*!LFAeDKlu(FzeRixL$r)V|lT-6faeE+)snNz*4y`qqN+^)p+ zYk9!UaX{fQ{m`9Y4fyq76g}b$vwEQ>3hq4W9c!1*HU5Q?W4K-g;)hZuVj(vb>MZ}p z#tuXQUdjIeM@a8ID4|1B^DlvE8eFX{AljK{Yewcz&Je73TD ztfK!N*gdW56q%Zma-J@*#Flb57&}>%u18n#{VxRlePiDtzR(yx=sp$cE@LsTspZOkrHb{+bx({yZ4BF` z6r|GYd9LK3Dm7_Zrwp3UsN8|4F|+kBUn?8T+p9owC8{+4d+_Xrts>NUXK#6UR2mU< zaqFOz%7pF84z1I!ts}TfoxUw(0F?4dI>bf@PK(au6r3W?t4~C(W4UM zpX=tHS>>+JK4aMBp7XMKdrjRU*RjF9bk4Gdi|?s>>ZwTjl4d$MrE6#G>s(cdO33T+ z%Wh3a+H*BW9XHG5QAo9=t7|376hNh?r&m5qRwH6w)m1r2q0>7x60mvrC{=P`R)gp& z1t=>wH(Lhf-g8!FIm-6L6!d)OS8*+2gA?_B38ok#RSy}$o_639H1040D8Ny30NHDY z4lVHTz>7=e;U4rOlrZh*3-83`>by5rtNLZ1TF;=#qBOLGUze#NFoM1kH|hlYPJK3G za$_;=PnLkSM%F9hvG)5kuI|iNx5sz+i|;3nq1||=;C^hy%6sKZq@aLYH@w;WV&2;e z^Ar`y9!UAmU@kALC9M2?3FrSAd_?xdW@bje!%$gG-RJB3d#?8>UacWcICX#eX##;9?4VERiV592xxc#1%$T`<2gPrCd&S*!;<5A$BzC@WWl{Z8Pke~Gg zKdvmp>GWvK8}Y8Vt%d~l%&;dyYf4h#?%Ue3UGs0xB$6;JwENmZ6cT!(z62BOEp3T2 zGn5W4YGusNdDU)g&Yu)7fCq&UL#~b~k+SM#={4ZNm8HeAI}Y~Di2p)A>SydrGt6n> zPjf+#YBJkfCL+RPjW)OT57)4^UacVw)o!x667g}sD$51Rsz`0wKUO=Y&|@tuZN zY0S^8j_Vt9;1liCg;XV~~-AXySQPkTd5?+WF2K`zjldme!J1RcQBmCxUbNW7Ff7tK963<)Lp0-F$eUc>Ejb z^Gb%_?xPzV5R0f{4qA?J81@fPa+xhIVrh^H>mS99_g1!HTGLKyAMr_1&B7pfoi;DU z)&RF^pw(Yy(1HMHVKi@1AeF!}dC3HWj90a=EWAUE&`3>#x5aAMA zjz;B#G0LUkF8Nrx*$9WaNeagyBdUR}7yk_TY^m~j%GQ&x%HP9{tSVk2ZU||;D;<{& zzlMc!92;9*!6w{@KPL|Z6S$T49zB$n8jyllM&Bqt|BNY%3UpY2Q0b~U8K%JJ-CXE@ zOr=l2gwKOS_)$|pH$oiyaIG*-TS9FG3#+yC)~$bp3y@stl~qoZ>R{GHSU^F2q5J23 z0a1{icv(F@*HNXvqV$xhA&z>>n#v?cP8!8BU@6|&CH}7m&olo3_)w1Ujd-J^K@08L z4P=BCHw?0W7rT4O8L>_$#1yQlGAlm*npriM^13D4pU2G$l zGneMnte@v>m5^cl407j>i1;DeW611ex`jiuNuF2|<`|Vk4S;KA&AxC45g-n~z=ah3 zrsAO`PxR=9KQutSYO`h9og*lgmkw|G{gH=#KPRd9dtnAkGL3SD;}^@93G^s#8fpYY z4$k#LhvCK5;u3*H4U|w|0dyo~GvLVC0??NhXr-jKGVc>@e|xX%1Zg7oK}E?pM|{N6 zLHrX0-I-!J-s2^Lh}>-0$xG~cVB8?v#Gv)NHST#hVfdp-Qs3lRfJ@(;zr#0N3%wsR zC!(Ekhg(I_M_0-zpdy{%WZF{X8N7w&<|Ys@$QvuKs_nw==JoLL_NSn;^?)nk@WZQM z_ya?xPr+@90n>0|B9YI{Oc%M(?PT%%=H#IN-zFD4=Q$&LhS~*KubM)g-grhL?uFBE zAl5zz;?XMB6xQSA(*RGh^Hhr9C0jWS@!D4i2%@WGz$}85{ltoo7RPXvGQbDbs;& ztE`ER?QiT>&OwO}La!n|!1>cqy{0XJ$e#raj1kravw1o9BLYC|E_#!Cjie#A5hVwN zZY$7Bg{k(QIHJdZ({HK#65fGOi?>xD#f>>Zu^d=J;6T~h;bPQfA~A&p`pr@&Y5aO zT^jcKy+uFOYWEe)(y3c|{@fYu6SMyAju5~9$5hicE!Xf#Sh(EZXtOTgho|3czP2&q z-KCdFcHs@t#VZ|rO(&Z zXkIJ&_+cTN^Gg>7`D4ePO>^X$c=YPmsUepyrb)j$EBrd!&&25b1K#rOCb3_9k22}9 zeO$OJc&VqxtJmfxk#g^T$NebC+4xBB#W9mT{pZErar11Ovne1_wf%L;qb)jtV$nA~ zXwN-=>(i=(8^1WHa6jWdb@AV?qqk>eqZbD%ukCWU4!)TfX*r&#J@g_uMjR%sz3we%s9#GkXHx{&nXw zHF>4>;p@cLHGkeG)z)Nxes*^CEJpwTKcCNEoFCz}@861fCi7yJr$@^r=hWO@Z&S8S za-K$nhU>paiZizs?v!BTUkYg@xh;O~DEBOz<9xgRg2E;lyP%ogk+b~v<~3KaG1#bF z6PU+;Kll5L_kT`KKM(APS6n$5Aw&3VlPjy3t;D(uEo4|2fcr8Ea*P%$th=;oezTmc z?AdeY(l#fC=rJ%vXwErjVRKPytyKbRPSqUjgdX??27S9@Sv?*ISeljem0 e8El|>gPCE*6u-}Phl*8zA`G6celF{r5}E)w`4PJS literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index 96fd6c89be..460019e75a 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/webview-ui/src/components/settings/providers/OpenAI.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react" import { Checkbox } from "vscrui" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import type { ModelInfo, ProviderSettings } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo, type ProviderSettings } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" @@ -78,7 +78,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod {(() => { const allowedTiers = (selectedModelInfo?.tiers?.map((t) => t.name).filter(Boolean) || []).filter( - (t) => t === "flex" || t === "priority", + (t) => t === OpenAiServiceTier.Flex || t === OpenAiServiceTier.Priority, ) if (allowedTiers.length === 0) return null @@ -92,7 +92,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx new file mode 100644 index 0000000000..239c76932f --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx @@ -0,0 +1,92 @@ +import React from "react" + +import { OpenAiServiceTier, providerIdentifiers, type ModelInfo, type ProviderSettings } from "@roo-code/types" + +import { fireEvent, render, screen } from "@/utils/test-utils" + +import { OpenAI } from "../OpenAI" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("vscrui", () => ({ + Checkbox: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock("@src/components/ui", () => ({ + Select: ({ children, value, onValueChange }: any) => ( + + ), + SelectContent: ({ children }: any) => <>{children}, + SelectItem: ({ children, value }: any) => , + SelectTrigger: () => null, + SelectValue: () => null, + StandardTooltip: ({ children, content }: any) => {children}, +})) + +const baseModelInfo: ModelInfo = { + contextWindow: 128_000, + supportsPromptCache: true, +} + +describe("OpenAI service tier selector", () => { + it("shows supported service tiers and persists the selected tier", () => { + const setApiConfigurationField = vi.fn() + const selectedModelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [ + { name: OpenAiServiceTier.Default, contextWindow: 128_000 }, + { contextWindow: 128_000 }, + { name: OpenAiServiceTier.Flex, contextWindow: 128_000 }, + { name: OpenAiServiceTier.Priority, contextWindow: 128_000 }, + ], + } + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.openaiNative, + openAiNativeApiKey: "test-api-key", + } + + render( + , + ) + + const selector = screen.getByRole("combobox", { name: "Service tier" }) + expect(selector).toHaveValue(OpenAiServiceTier.Default) + expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([ + "Standard", + "Flex", + "Priority", + ]) + + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Flex } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Flex) + + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Priority } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Priority) + }) + + it("hides the selector when the model only exposes the default tier", () => { + render( + , + ) + + expect(screen.queryByTestId("openai-service-tier")).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/welcome/__tests__/__screenshots__/zoo-hero-dark.png b/webview-ui/src/components/welcome/__tests__/__screenshots__/zoo-hero-dark.png index 6f5e11eaf9e46aece8e6b9178b46fcb42d7b8532..3a99cfb487c092b714d14697d241277d9c2afeaf 100644 GIT binary patch literal 2009 zcmV;~2PXK5P)BTC000M$Nklp(0=jH(6l02&h}Hh!-?bS%N+&7$sE_W1=rsV}0?VJeio3Cu>qPz8M>^ zN@AZ(s-QLsHAZkT1*@Qm-6m3Z(F8Pxo5*eZv$K2VpEEO?T^Cq3ne#9l{&SswzVn^$ z{4;CH&o2-J!R>a7SC~$C6)hkdSgFUtO7S}7YbBMke(=KTd-|mPu(86B=p67$En+#& z)MKG`$>Ph)ES^@N+NXFm(3Wf_9t$u7@v>Cvdmgg-v>#eud4~|bM1A2gFsyjX#k_cr z^iH|ER(M&gFQJN#Wnd%uMrD{~{Q`My2GsZ%Jau3O7N6%8yb@Uu1T}RJR(slG4 zC0$3)QPOqv9Hq$C(F+#D7>x#D8-OQ&W@vS5|lv$I;+Jx6eZJN{c{RDJ=q5 zU$ht#6BF5r_ElElXeFXqtFOL_K7IPM(P+dn^zw=oE4FRhHZn5u`gPCz=4ink&^tpQ z>?-9s3qz#5PsxIaP7Xy!<6o=woA~&+jEoF+C$X`y8#it&DJi*k@BYNZq^cf*uqKY9 ziPwNyKpo>0voen6{7%OTRYxybvZT8DhsTfY&z?QwZYDW78DEowuA@WO(Md^(%a>bQ zTicEu`>w984(a02qgIFGpT536i^VcIIr;hX80b1WbRCU!f!knzf8W)sSF5V3K79Dl z-fr*j@5f4S-n?fphHU0q%0 z&YkP+?PZLN%>_LkO>kzSiju{Hd?=po3t4#k_U*X24Gs>zeEIVB?c2+iEj1X7adB~I zdHeP)BD%0}T}g>8E6Y+;R1^~vlbf518)JQaeRFd&)pKfNg&_s0qqXoJh>n(On*4Kg zRn-A>=-s=2IGs+^YP)vrK6&!w@bECkkC2W>M?YgjJbbTRyA}^V$kB@zFBZGNMX++^ z%KP`5A)?~Z99ss<(L}!r97mhvBgt@QFd(rAnVA;k2ea8cFfefG(xs7+VVli{dmgH{ zxVTsZHJ-einwkzDK8$Vg-^Rwq_V)Ignws(PaW*#BGlVtC@d&C2^@DLW{3=&~#Nb{; z)3I3|TcSojdi1En@$ZcrHzp<~hK2_5V144m_cb*?A~WDo@YAPH=xfYl>AQFDP|vlt zwl+34;gi5lP~@RK5;$72z-;PhLHT`jK|ww$wydlybYEd%q1X)RqNSz9<#HiW+_`hN zwAA+E#S4tpMMpFN2ezP?W9Xcdk|AyHYmFD)(Y)~#C?FJ8nj z1+oVgEn2i_&6+hfTM1^KKK(m0GsAATqtX~3pT@Fha&Ed(PMtc1Z1dp3gNW17N=E)t z9j(UE_;LhwMS1ydj7~g!`0(u6pFV#4D6WeO7tW`oq+lJ~`q2B`-Q6fWa1Ugf#CN`8 zDrLIg)YMcAeYCXv83`OMXmfNRI$DdPd5eItH+1oyJ$oDu$F*w>c!Fv_&P5y@A~!M#>U22DLFlQEn6ZSk7=0f{qU6o#FB0$G2?Rf)-T7SFZf(bh@aX7b_Ew%hU|$-*pJGu{3pb zbad3_&BZVs@O0MJ*1B9yLI18J1Rbpz>xhkwLv8o|{rl?bYCM>bXRdSfjB<2p>a?oE zcT_0s)K(TywhD@)!;Wh|I$A8B2^}3UN2jEuV%~0l44@jJ_D-6chg&;N2=h!w0|2+T z3t{;_+H6k4Xa*jmnI(9R*5lE@aHjdraMPyZj*gDe(NQ2egxf2)-9Uj~)!l=x+3+O! zej`~}In3NUL-7B7bbfxG#gh5_d6!~lpzCOuK^=|J(az4Not>R@W^T3bI&>Wkf$3$>UX!J7G^EyX=8AmhA r{{R30|Nkkiy{G^H00v1!K~w_(k1$vkp@mAG00000NkvXXu0mjflIZQT literal 20809 zcmX_oWmuJM)3pJT(%l`>-5{_jDZN3Yk(BOEL1NRTG}0yAo0bOY?%05obT{A0a~$vY z!#@gFoY%~nHEY%w`d&ruH7W_}vuDp!_XQ?K} zWPOsz2sB%C*`Xs}8V^*X%Nm;+8-F!6X}3slXiDiV>FJfuf62HPqb?{b8%CxMl&7qU zz>L62(qodSgN#zpb*n)rNp<9$fjy02q1!2()^>Il-Gr8Rk7VGNx zeFa_jlCakvWABGW4vDBCR7FKaLf%(L)n7)cs;czcd~E`evkD>5)&Dd#ua4lIr&}X# zZf>O150v1#FHnE^94?Ff+al!9$HT`brj`OXKxT4raS6d7uku}P^YfFI{)SBx7#1&u zfcGX|O+v!sWMiuMlPna=aZw5(c7A@|X=_;dai~9@@j4G1JUxVynT3Uvlr;4r_1{c+ z;`i@F)Zoz(;uARxf*()Ty2B$QBV|oQpKgw{dv`h_5%TEk?CjhgfcXFaR#Zrc_&7_a z-j+=lHVPe-1sAPI5^*~SC*;T$yZKk%rt$G3aqQgFqY~KI*r;{Z)1K_jRd;kq(Q*@k zGi#?|Wc*^;VFJOR6feOs4bDLkltMrr-@m;)T%bHXf=es+^xM}6XJS3iE-JDZ`6L=^Q)^*ua4KFDa5#K z8ywbqzz6?Ss$FXj?*QL}lxrat{JPN_7M5r-A){t4At8_5$sz%}Wpqr;FQ%qq?nl3& zbK!wHP<5%s7ONTdb==E?B@vGk6IWN)PoD@0#Md6a7!hPf=s>?qR$(8`+Au09DS4ky zeWItQhuP7Z(K_w!?nYqI;GdnJ+pM%M(Z!h#B;XSg8h1t}$$&SMe_dN!+Z#omQ)rH( zq_4l6!e{kU+{Y8%fkI)Mch*8PB!}ELHr#b{`y#qbzE<^Vo92QCIe2rikOJ#>Ut}qpIvF(Hs zXQH6ExL8L=$Dr9gHqHra`RNqY3D|W7?UtL@d!th)!AV7qjg57O6V}(%3?}pR4lGi} z;(#wt_hWN&)2{Wp2jQ#LOc{t?HoeB0qM}gbpt~+8uVl`u7U*kRHnPLzfa#kRuR8S3%;-d`s>#| z57%1^g;+n*#JnzsQU%h}(}RM7&d?)}-+7(r2k ze00Q7dV0cdGT{Ubv&pO<7hAjtuNlts6Td>3L{Uh<@8YOPOLzVh^HOSjdO=)9mG6m( zZZ0l=H#W?RW>=}e?JG0qK?+DCD0@nco$O8(D=RC5PejM4{e~a;>4Q-?xD8|KuQ;1CiXwl+%{_b0=+EcI~a0?1vbrTvSX|;nS?dj>cs?W*EDJj_l=kB;t zMAM5*jnf%NugJi_Fu1p-4Q+02jwBTj_j~YmcAkXh()O}Jq^RgDtgV%LQ*v?T6^DO{ z-ENQN;In9KY;9SMy2U+xE-X(^&x`$q#6TaM|Bx;ElX{SI1L567?_8Z*<-nNaV<2`=F<*tE;8ualZSJwqvz1 zHtmQbXt6*w!?G_^n%C(+WYx);na86}OprNa7{5Q!ET+hszqA;acsRz?quBzFZI9{M zV%&?-eDQF9+tk#QL+-lK7Yp);*bwdSpI^V;(2{{L?V!)g%fp*1$)FT3Y)Om7n*4@0;)I1P~%6CB?j#_8$a(x;ex{g9@SV zT9g%h3+gN>vI2-Ff%U|;`wP-tvVG<+!)bHP9@W?%GCxj)sbhd6!xjL@?smBRno`yg z{5rr%h!;U`bab@cd`&-89%K=kZUML_G!>j;j$E{?_jWI(-`?I{#6?#KCgmE-Oo^5; zglKMVPFC=#8h8VkeL4(&q==}f?7cAxF~Qw{FE=W`B~KT!%t6!1VX#*;l^sPv-SQ$N2z{@D6TUEvs|ZM z<(pDIE!;HHZKm}p@y&k5aWM|z_q!3J6)>tfjz$c`}gm&iQe7L zH6_JZQSSFQd~1$d!{oS(Uq*iJ|NA%4-yfQP3t+zye0~5$9^ny7(Xqccx)t^dSFr$; zk~!R7*xde}4{m$BKLo+AUuB|D^RiFL_X-{YP#R!LN(2H@QN+Sil}H0sv(0+0svEYJ z@p$(I;9AY!mu!7XppZkAl{O_abM=0tKLIcXR$_a*B(8+}?&EdFV^MK&wZz_Jk(%mH z(d%`}M{{m;Hgz0AP@AAH9e3yFwf~rp@xQ750uJc<#F^&Eq}|}Or4s-7{T9dy0zyKa z7m?S01-b~4Feyq(N}3(kgYPF|W8>oDULs+1@flSC&e776t{)U$DNGFF2#T!vMz)r7 z{yC@v@>3vZB6WmCL;%S<{&j(?YEHd5lJN-8pGbo0dxdlIJLa+#5`Q;<2iiqVZik*F zSaA<4tvSFyp`V6+g2`T;Xq`i6L6c53PzoQS!_tFq>T0;G>s*q2F}rnv+g=vSjN2M1CYCvpg@VZ+17Ldo*d!2u=g`3 zos{P}NVA7Te}Dg-$~`>!H?pw&{2sGO68$7@TK})TRip&x%Pn4MD5dg{f66KW_r2@U zk&!aqh>ji{4UzI_8?c}&0ree_I>h@Q{zNn7M`AO4d)O(+`1R`-_?BOq{m=XW)#>7s zmX-#*0y|pB=jMzt!yD(AdY?8P1%$+vKp^Ht{N=^LlA?FB`4~;AaTJAEnA&oM;kV<# zSXw#XyJNYUriu#AyAL$RH6m+q3}b&%jrajoUR$A6aNVEBKjrK=0AK)pCmh5leg6$p zobxAj^7Yw->JBQznvFu-8EDQuh2W6wjDMR-lagZ8{8$;$=E>=G1y|hd1UYX2IVhY&I2QJhKJ+U2g zH$OiK?p>cF?n4=unx-ZZZ*`gplShn^10cc{nV0Fz_!_WXFMW=$=LB^;|vTM?$+)-Pz1Z$+U3f%Ys1{<;m4R1qEwiGwS?|8DfEO2#!OyZTmW`WjCu55^Ag}K z7DA)PMJqY$X%ekXH8p2<*Qa3(U&Q|R2=j94r(4yB=C8Z^D%Uwf3r_j44o zy;+%)ABFx;3&5|&M5*s%1@QF5 z9fXDdU~cPONiD?3$IrsuFUx#xFWBan=H?W}pNt3U0hgD^j#Bef9oxj zd~k38Du=sUS^J)Z=b3qT7(V;3MlJ`AZ7lQT}WZFAa##k!{m6(+EQwhGw1nYy|1)VAEfaa-L4EKVH`Uu#x@thp<5i`^W6itzFY`Ib`Y;7Md7VP$s7O=5sN3oqy0x+b$L{uL? z>p$P#HkXuq9+gy8aGFIs&2EZDz)Hong>OV*pr<#)=px3cn~VK)2}yQif*a1dZLgXb z|AL$)lj$B(cf+s*K;pVM=y-;aY}YR>BudD~_QXV@Ql;_&MQ~&6!{?MNESOvQ%=!-i zJ_e4BU0mm>$mfl@%!;Lh_lfbsN~Nj-Tym1Fpd24R5)et|13%-GBX~2Lrq8Oyx}t(BOVSTnTEIIr?_YdAeePpi(F_%-_0=M{b|!-*bNgI2OmaoR*V zSmz}_ZZ!_Ls`1e;hMM#PQ0tMg{`~pVm(%&r7r@QiMdRUo>h`z7p^$6^9&tBw^X(7F z>jK^gG~^w)C(bEIw|l7?xs8p&92|Bx^A*Ex2Dm$bkL1$B6d#-~_vU!?e#yH7^u?=u z8^C4&aXyhvB4-+g_^jyt*8 z+Zf9M50YxgRx^K~A=u%G2?_rR(o@5cc8gQK##76~WP~ft$Ff1q1FTR^=OOf~`%ns> zC<(hOY^K&~rboAYN?RL2&Ms#O@E)y7Bg8|QueLZnL0C&mOLT{0D#5|O>gp1jlD=mI z(@EYx!{!A5mj1Q_Dz_Ki?qbxR!|Y{iYdgrim$V4lpg(`!rN&a2Fd(eu5~J`n0QM}^ zWI;V$ZlD^DQ)%0e;_Tw$z_7l)E^G2iOBQ|EH^Cb1^Xq2F9s zR>qHUwHD3+O5|QNwL2UU1hzHKh`@@sbZTHj)RnoEG$}SO-Vdnr3%IR?g+o<2(QwLV zYSA6pTM|-I@&iI1E-uLgo3`}GT3fvRz}odD1v-d;Gs)2vWP!b|2(S7T2p&fHvi@?A z0OY6gzdQyK*un+Ni2HN;cIZ+WjAWT+0Ig_y)dy6f;o`CE$*C!N&sr>%pRVDq0q++E zJgaF+W9`GAjV&yM-+drrWoM77(_I0|9{4y$Dp+z#u~$W0!xQaPIXhg0oXNto^OIbg zG-?_`HK2K}S2b@?}QA3`1Ol4(dN(#xY??eHjN=VFn zf5PV^HEWA5Qx!B+RC7`?_I?u?_HW<5an+_4241{cSz2BmCT4IB`dA|NTGQ9p_e%EM zXgX&tS6*ymOiN2k1Sq-OeLxSaXQp-};LcCHoHMFbFujx%#VDuhPf{ zZ;iG-fiDf(q`f`scuqn36oGjZT5fb9+gR?SW^!_JAt9kY=PMON(1E!hL1U302x}d= zpG8U74SID=1`!HW)-favf5YnR?8KOPNo}&_zL82m<3L-|uT)suifjn!0j0LcpDOj| zga1pU>`;;hgjhG;A9M>tLqm~5uc8XphiD1VfwB_y&Go;nw1@oWRRHqj>?l%6oyXs? zLi?^+H~2#}5yH$$nwoTqznkENy@Q};R`A;`i&kOZI$-k?^Tz3EXqb_GUM~X8fz?{z zuk}O&Uj@WCvrf2L;YSJ)*9eBN&-a*qS1+$o5H;5+d0#b9NAwYGz+_OVS_Pdpl?Jh~ zkp=u^Yn~5)P7o9a*FpFoE-tRRI4#EMTbPxVm0pRV^1E+3jDw+8Zz*auAPLApZ~Z_b z=86G*4S;{&z(6>^c9@AZI?$Rf)-0UH3Q&&~xxIbUZD+9`yGqxaA8R3Sf zBke6rBpO)|#}+OjBO^1bU{cx8a6MQ31)iU@f<-7utPmMGbNNK3!8Q5sB^95x_e%eSG@kV>@J&DKN8?4Kk! zO#T+A)Aihstxwm%fs36eT`Jf2W~U$&YfSi@3|D!$59&dldXLKLF1)Kcjs1Y7PYQSC ze%JWxR~S#k_RuvI@3ZAkGfhrg7&BSL#b~edijJ(LXv@FR#BYtZWF{%JQHu_2dci+z z8IQ{}0vRBRiIo*{J1VHO0y=oM$~vyE6YbwHF))Hh0xI8q4}5c>%E81$wGGwqa3XRt z{)FO06J-qVN-HNM!qrE*jEtaJNoz3p_jZgYGddvPl#Lg(@U!SYcUgX=Z`Y#8*pCX@ z!jaEtSXit8m~gr9)0i+ojjs;V(ZA6KQb&efFqN;GxFH(338Rit42_Hkv^uVJp_EMl zpkd}I)2!@B$=gB0L%(AE?pkj>$9=BIG4&xfs?PyUPSZGWjLOnfu*v;XTQz~!>++x2 z?_&}mI>`2@Co$%|?;lG_OI^4#1F&f4&!kfFAbV8OpF0MCB#|;56yv!wp2x(Rd=40} zXj%TyRNwG01u-$adc8H&vu09V7G#8qWI48YoRN`j1YI251k#K5od!thUrGX$kCb~Q z%xq$2sZ;D}HJbzCeV;WFv#D+^nup;O_%I17qhX_Fd}rxFw6?O6?qc3`{CK?1p5UK1 zw9aSo3KXurdQYtCXfj%es8VnR8T+pEMdWr2N|*N<$mIthDg{3NL}1u-8WTzLts$E4 zXGwZr7@!p=xEJBi0g60MfC$y;Jo$8Kl`&YKu!R-7M27{ zKr;QE=Oxl1FSzbE;#D8c%l(AX2-guv;tc%mU2g17PEHbn-}YL(xw@=h@#T?HlI{>o z%~FgaF9WI=3k&VvOef`$l$FdSjoC2Si0J5?)WW!?^rz% zlOn6Cqzm(%8w@w(ye_G*=Mj-l6U-UO*3=A3D170QJ;PGVN# zBpGPmyf^x=jc`rQW-E;uj*l8jTw^s{&CLM=cmz6l``ko&Bm2*<`H6LuNWbUCj|8>u z6x}5|#Jn^c9O*kC>0!dlMW7hFuymcRsB37j>o+Hr3Fq-31O!r-nXDQir#$}|BQ`=P>f7hEIKzPa%hc-q)v*?{o>3ij0_VR?Bhx`P4Iq_dVCJ=*} z1XuWGgsyK9%QY(l>Sa?bI2wNHCRkDDWBf)13^YyF^J%uwMFm$e2E8=Q zB1j})*c~^-*S6;7Alx_EN?HcVFDLLY+;g+wDgld*s?7padmQE_)q{wfd~!+Ox=3w=%P{CZ=;N z&AZ3vZXfzRHr-DgKR58b{tGl4AP7s#%tH~tbMe4p?Yp-5_*i&j)2JN`i&0*0RrKs0 zy8;%t?G7~Ti!k$`fUJ9`!{E0|b+$`4=Q9SX_c)B=oy)>=uHmRAyvDYXg*+G)l~YM_ z+9Y1^pAIh)rLbOu>Fxg}Z|P4%D{Z;a?zlCzqoUFj1Qb$(*R~}7n*w+LOi>8`JR=H^ zjm1VK6`ytt3JMevTg}hr41!Vp2Q)i=1*8#~sj-`6qQF9u_BC+{-#genE}KhEX2>To zp#LU|cx*=)&Zy{$u2I;;6MENlV*4{8v%T-CU+*}Hxu2kQB?Lnt`HaW20tqdH$X>)3 z>xgZS$Gwyyc(&R;yTu!yowKfV*5Vu`IiF7_Zr|2f0#IC6WPIO9(t-RKV~<2)ggO~0 z61IFiBJpS)&_0HB_jSq2&Q4CCfo;G063{Qes+=2D(*&{0+Q5J_T8L(p5WXq%z6^>< z4dZuOV?&-`dGn?tM1^)vqZpi>uu9wobFwhLLulN|iDR~X9DP`b#@q!!dT;MWb8kcj zt)uA-pNlypO7G8+2HYKtbc^jlx-X<_fNYAZUSf#97=H%EHYH0KQuy7!@qb#thD)}l zx*d=e;Ud=5`O}NIsh1tpC$iX-*=lw?nD@Yae<^LP6( zU>4!C2yLOC;Q~1d6(1MSDd!~}xKpryzA&tvTM)?K)TUY-TZV=hwIU0t4*K}Uswn={ z*3q#TVLChh13Jl79xw)+ru*tdtz;e?i}D8WOh%d|33G{aDvA($>_JxBI5?dMG=n zI;>LDaV?yxxU}s^!J^^%RM6+9#%$LM(m}42*Vwq&~^K(uuy~duDs-S!(7#{s9u??e+G~5_-a8b zxT&jU_$^Qb_W9t5;cj$x7D)e;M);*FZb#vwpQNz9sI@qAw~VIzG4w;1ox0ui0SeZ+ z;1;L$@IlIsu}~pXT$$#8`~B5=v{}N*h~I+~7$*7n_{c?(adqJ19PoZ_{O{ApkA`NY z%sA#)J~q~$IrF^12s~~7=*WJs#WAf!HF9%)5C1c+pDNOV?WwGaKAjoCA>zBH>BT-U zhP}TY@yl=HHoh;?>IBT0fI}Zl$I?c%kgJJM+)@mO7E^bXXJ%$5E?25d(N2`9Y)c33 zXdqY!>D9He-{6V+m%@=E~c%4 zn*eWum8IoTff;|tN+*#0=oSxR>~g~;u#87|UTe~1e* zq(5dG7#kZiFk1uCJ&~_m!!<@@XEGDI9_1Qw+L+g3;L4rPsf`h3`3N)=Fa^}rkEoUw zd&9)E`rVz~fMB290QH)v1viDMmE20sjK|9X_TC>^u0w1giE9%bFcC?=4jf zIxl&aLSfV)kPVj_UBFEOMK;Y}lYW$$NLNY`p!TTsm;^UvxCf$&Mpc_hKhFGcettd} znQ5r2kM1P5VzJmTS_a+V2HQW|kL0W7U9(a#;5E}&vp;JA_e;+j)J%qQ-n3jU4~&e| z2b1c^eDDLLdIac0NUvUDkPG8Im=FGC-3wZPL!3x^hr$Ib_s7UCzi%5LbDevM@|AhO zJA!JI?v^ht{#rzKepL(v8Gs5VsvFIVaE4qAAlWEwhV{%yCyn|dUO~7M08ehbo4of? zqAqIAT7q|?>_t`tMHa|9b$&6H?J^5{y|ua#AZI0!-Edw&owjGn8t6o~=>Kf$_o>Q{ ztZm!Niwl~Mf-7_!^3g7}o(I$RynlYm~ck25_iX?}cITKiX*4%o& z|KS}LuloTvhTp?!MnN==1I;3TD%)y+jV!84F9Uj3G2BGM1X7w+w(TvOmn8Ke6a-BX zqXYypaB+XRk~*3jT~@~4mEK6uT?R-FLa8VA>ym8<@-RHw?~eA>-EaSwJufqSZDKU0FK=QWbmMrR=r?~KY~=xMUoa-XhVke7#uYw7gN*4C2d z)vw4l>0T7}7%wA-jUF>)alc9e4O<(Vr8=7hFuQA3!pwp+78S~sX&V!adLSD1l*jRGp!Iv zka24v`aD^keI2}KQufQ;nY1D96PEKyUcKPwDSHg(GSS=ZH zMaJ)Yv3qF35^Nrh);M5kA$WUxJ7%RHPrz#t6b4Kqu91NvrkV3aE}9cAWQd1AnBL#7 zt%Esdt;&y78utP>K_a^z$%JHYc;qj~4vdgBp+)*$M0QvAD=_-D*e}uZ+;L~qte|>X zKXKm5Bilm*S9$F&;JC25=NoWHyUj^WPDV`pq>BmQ%k;U+HKdFC0PW5b6eU2S@{TcY zuMdjwZLOxTw|XZkuvwH>P;vh4d#NS9H@CP#^8oG!AoG=$3?iz8q0*4x988W*PuF=} z+Tx0G4g-y)!dJi65}%)pk?7Nnzv0Nx(0wQ6Lj{;X)9p0{+l#QG&m)keRm9tRb)nFH z{P^+7rR5-NU|`_mbAL8vfZfhZ5-UZ}W;R4(6|3K9dgtMeZL9p-0UQY;9sz+7j+kwC z972iKVJ;<@Pyp@UQM~ld2nsxNK zCljG+?RUxGcsq0RRn&;cNafmIeO)INm8ih~1?5*cwEU&0z8$Q>BVb55SZENVdz~q* ze6;UFjeSc^xxD2-m8r6+FFR;Ljv-qHiUg!eYJaEg4ireGuUQvOU=|LtdZFHKMDxQq zZ77S7$qNwbF;TcZB{GW3K9_pg*DRmP1k;Rsw@0%c9(-*-L|e2*@;OyWHwQ&_oS>?Z zxpjTA*EGzTORZdYQcwuxAhlpXZZ+4oQi)Ve<6=q5%c}f)5}Y_(E(7f^0tQ* zlZ4R;xEh$t)VuC$Z)sCTMQ%FMx41RxVKOn?SF9k-1^lLNRvAD7yM^f^-_$sG&-u_dDgrBcvQFF~#Pi>=Xfn^Mj{;zs^ zIXmbcaNi{K;6vzfY@f2T{~MMd2fDSK9Fif8aMDTX9J$eczWlCo@w&-2GVy>MrO(-u zuj;C@AJ%Y9J}g*YKcx~Va!2%EP|^nl85zWn{={wQ!?QL+v&0cYHC z2RxJ4%Wumz+>Oyp!pJaTJVSw5m-mF_A#Y&Ko6J+N?9QzT`8vu;S3r-STx?Xe4dQJ78uk}V31Yw68wkBr63!?DTJkdUIlo!5ZCK|y3>;>Z zWE6i`XQ&gpL}U_NFhk&uTnG+fW#7c2!QRD%40@9<3vjsVeO?XKF75(={8ftF&)RzM zhFG!&okUOD+TF>C$L|>V^`lH^%vDipNK@?93)*;-;#QF6B{jG*51?1Ny}4NfC{cS1 zH74D*71=9Qvu9ytjbK`YsW1`oC~jb@^vtAP0YlENWqlLY?zxv+v-iBohXYmT74(pfu^a* za6YT)qfhOafPo&n{V59+p_hk!p;mh#z_;Exzn@4$kr2NZgkw%awgUM3=s{BJ!XIx- zOUtzK5CJZ(v55)X{SP4@P~uuZ-87FDRw(|`92;NCKs&vXW*4`U&ZbkhdSh{2!mQqE zJVm7IO=h~NA7jr*W}6xphf`%SY5C?}o;whIV*iaHT8oSD00PHD8AF&WsAEOIpKW)7e8JVpAkNI&CqhCNuN|3cD{NzVj|Je zQDH=WsPG^p{Ac2T&FeJI2Zkb=SE-v93@{yaA_GCodI@?fOTc#z$$@=^a!ON-dy zyP;H&7ntVzfx7r4?r58~h8y=rB}1gV32iV9oo zPItOEFzkSm1hwpu+X+>PYe$X8yDbT9jN9J`_4uEx4Aqp@sjFPpfsy;08Q*3VY2eO; zvcQcM=G=$o@^$RAgi@pQK~zcC=vfBPcNJ_#It}T{!qV2*I%0(HEX`UgDTL65Y#9z`R{FY_Y$@BBNn8)(XbKJN@ zr19JfNc|RTe11!f&>HOzUmDuCw6wsi-;DFN=rYAaYhv*q|ju-Cmqk(F2rxgh^$ zCOn09PDY68B6t@l$sC3Y3_6r?Z^z^)@3;IO7+vrf6~pO}-TGC9%VLSi@P5#tTFp4! z8e;@eq1aejO8xziLA7^kBSU6F4vgr>RocA&k*X|nGTw=n3lSiGw>Vu|l-4HEIEqL@^dL%xsNY1Z57efWU=7u7GZ-_>6PMWbgQ5vBf- z>UANf(AUn}3DlHp|1wX2xhIgAcbB*y!UdH?gfvNO@8FnzZ*lbqVZfhQ4gA#+%|loI zz-6(H?)jJ3kwV}G7&gpmLee4>v*bDwt?nc27p%lGgx9*HID!!78LeIseBz@@WojUe zVuN2u)g^ZG%LqEx7foe4vuh+4)cXQq6m*V;+c3s4hd6}XFh4eC)rUw=(1p`0CSYYX z9t7vq71{5!|DP6c#%NvkYQEli=RZa#<_uAf4n-L?F(cbR?#)%K@84OOvb}ZKYzJ$P zEv>9d#}QZ1Tt%8AxDfr&HBXLSy3=wexX(E2wfWvZx!!09QsW~uFO*Qsq2U(KYu*yW zDq9Tt9ByB^F}rbBc>zIgJvkIh?NN^`8wlHZZ`;Ql@KB3r$mGum-Z+zQCq4H1-Srl! z73dD;%6<8ullw+nYN&Vb@X+n(-J|VG>&dVgxf!Dab|9MDuwxd4CG%BgYW>Bf{;2D5 z8-ya12d#Zyg}6ID+`2GmWnqk~}M0S7P1j{TR5EnsLQ@49wSJV2c-7RFq6 zV(T7nXcLt8;Jj}IyK4Of#n93MnUm>yW37+ zPA9Y275DiMd=@8Fi$_Ja4nB=->+62Djm=y=6ibFxx!TWCQ&*Rwd9T49%9NNM7Qn(=ek~@4UJ1k2H&Ab0@lLSfr(E#&KcaxJ*FcIOc z&&>^Va!s%QEWc9#HXl^k64uC;6R*VQTa;gQ5DPnGLJ;J=YxpDH2D^nOyR0sPnK+gM zTc-!LQIBrgTLS}1J4lTY&m9K5M=Rve=OvgMRak`f_tsg@jZaSplV*;+zG|HECcxDe zVl4aeVO{wofUO!u$W|cpYm#mUYTSxDVWXn>mb9jXF@C>JjH41IDn&U>W~uxUusxjs zi+XSED1x@?fMInZVje#ZI^)OpDkAf{B{i8Uud*goAgQ-1YI53Zxu)5w(&#u&LAZ*O z9ijX<#vl;yT*e&TwP;29V~`t?Jc@(fHBdv9A#X_bc04`i>^CQ;aSTy5;~6TE+=u`XZ*$wtfM|8rO?CfAJjQuJ7H_-npIaI z8$Gr2J+H3EmE5L44H+FZlVA~Y%WIWjVS7)7I>f|)KOW6!g|R0{UwC3_q&vt-OP<$f z)P4^+^--TdjI$D@6%JUEKDqjev2{*u1Jpza<#0&t@!HFmFUQKLQC^iIHxgGz{c)3| zC9!!cz|q0dwf8!+VESegezc|%EO8=fMB**{1?mN{vN&I^*x==<$sAK*V535+KJ*Bb z_K_#92hFeRDlJ$sfc~KJf8CMtKJ?{Oh4?x^!GVDRaur8-k2G|B+}6t0lXoK00JJ0@ zl@5d1{?J6SL7%70Dj!&i zl7~%6@5T_|c0x0zeh!eDmX@}M=jt6Ji%)?Fd&1DTO1u7h^^>;D7aPuJt+Cby`8e;t zcwH{JdCVnE)qQq6UX!sZF9(C~o{}U7I_|CJ>WGAopFYWp5XxFa0)3i8$r3RObeE~iRlQYa~8Gq<>yWzA6j0=R|ZgAsIw!UG?rI>N z)frXET!J%7kAz~S&8C}$ZjiTZE^M^jQICm_MQDX1C83)Eu79}Pa?H*%(0xXqJgCGi z1BDQoNOeTuzn<-w!Z4$bW{dE|!8+{fTZyj7jvfF?*(_*VNRs8Dni2C1(;) z^AF3(&pZf!?Mnq*OaD1~HG)>H3p+hMO$1~6cLPI*hbBgUjDQ>{-uQNTEw{I~G|Kz* za_C;QIZC1T8!n=~hCYF=vbg4F@fST43}1WRo$g?@C$XdZY|&`q9&}Rv-N8uwmqXpI zCATMk{0GoRl|~V78dMzYHs#`veS!Wy4*rxkKo8KZF+7VYv0;j^?^%w zKcrzDz%Ibg5{er)+M^Z*!|i}-+AADWB4LnmmK7prMJ(`DzlSx*;MV^;03OQzO`J)J zD`XtmbSyb(s?_*+=`R_DONK+E)VxATi=c0p6Zz`p1vFiZdeY@O8`8gP=Fw|=Y1T|i z*WdGbK^-ACzsNrF8nVM8y4f>muLTK1qQ5kMy7pv`}&qq6d`_8)2l&P}nj$fVYw}!8ln^J0;eX}?A zZ@~Nzf*=!fh35J1nBA8^^GXnbyvCjhb}1X>$kOTsRC<1-!C8rkP5HQEu-+l(jpwG6 zu)7hsKh$LZx>Vc|o_ih94^~dd?rg1M6b{$uNCL+;HF{0xjnZD3R=y_XkKq`}Hy!L# zwHNlLdExA6smn7L@b3k4E54Uv8o5m%J<@y9+;pf`$68+yHBIo6I^H(#kxcJmZHN>v zS5$DyHJS`LSsX{xFUOH_sgVGgeKq+;RDp-^?F%pv#vr6}X%C_Fg+f2qtdnP^uxIA# ziaBEyhmT`xH7+c~)c7aTynEu|T^7gFKlzxv23bPc)r@l=@f>1M^a5jJW6kv_*YHTP z!1_aM?KVk3@wzF-}pu7%d~4z+cr%Sc$Cd*Bojc+;blnN?(zI$rEx$0-lf{eU50AKcyDfo)b=xhOj4DUS%o#@_}Xd&;uk3Ss}V zER8c?3uNc4Z3nGA-~96{+g)vyvQp(&?^6E|4=3{=y)08k!$nO@$v*}T+#bdlYfrPC zraf(Y`Im5p^3`%Aa)+5%H+i7(#6_A;L-qyDH`v_JBKy*d z_Cud8wU6k!hf2iYJhD8LBV60GMzU4(9lA2i@URK^*TMLE+|#tICcm*Uh2#EgXQD@9 zdvJ`XkYNYyxyxdbLCRk`^xbR~DKWO>oa%JYno4fz2B)A&o%ErX(Yy z<4Ph?k%mG}rKbm7)?eV@PB6scd09l7Kgq*~D{YVa^((@D1c%|S3yT+LcH0s|S69xZ zFu}8Y58-pAeY#XB^9q~bt)}&yXmLmGYy&}R`;Mi>MQ^a4Ne3BB<+u3*8it3!l;Gf3 zzfLvg;?c3K)PR=F>))K+!BCTjn;mTAh`tsTB2xsOXC!C(#f0d)NvvwrnYlSFUEK&5 zpNBgk)vVgzh_vDCCrE=Fj0*1ii|k`Mzsk$oz-C>|OLOfj=V@*)Z&__}l{m^8mUmd0 zR6F*6ilv!!f%x@-R^g6@sY49-r4CLvG5q@-_PdXy?f{AA(uv32M3CaIV=x*}P*O%) zA-yJ~()ljOSt{7Bt~~nt(Wue53@aUZ)T-nLu$v=s1Z=R$yr}XWms}q;LIQ~c7#Jwi zO8&|I?m*nSo!U*`L3Ybi4q;|5v@~ZX@>9rL-{g2nt}v|e%UcNt4hCPZ4AUr25@9a4 zx^LHjkZ0799M7mKjO{=|LDH7t_b|6fL_?*YV6r)$r}$9j&mQ+O0Rw)qyd9@-B9IwN z_Yn62o*zG)nyu?wyT$dSy!#2vUayusDpIqX8!sWZ_ot(zDD;3q!Tx&R^t6WKb$RQJ zCVBy1&kY|l1>>$5Py=Miz?v8_eQ(VVdWE8z0el5N|3>0&m_d3 zkssahK59oo$eQ0|z@}>!oSU2sv{XyUyfBY@q5MHr6*FJFFZI;X6YSARQryT+x9f%Y z4rBu?ZB^<{Z16g*qY=&nC>4Pa47ad;U9JSaMVpI3aA z)E?BeYri%Q`adne%1Tkia2joveqQ2Wp|!OYEHNsF_4B^BiL)i7y*5Am_PCYaN(X@{ z$lC#c2Rjaams_>|oVLK5h_HS3Z^G0^FeMqxFbaa`vu5QR;$}@iw|ACN^TI$DtP)EO zDie2Js{z4&3G{4M&qKT5UwV{RD0h?|0>i=L4%jIhQ_s~)*&JYhukh+|YJA)%54U z(=QUg&sc8z>Q4od)Hmy>Kr(qd$7R{P$2 zt^=eZImiP-HgFL)``+Jx%@r^oR-@PQ?V$&NZkX6tzFm()@6dgh^1FwHG?q(AMaAfc z5ru5alJ8#I5t;z8KtF3(ymyAVO~!}+{sZ3T=R7)@y zt67Bsm*p#9dNTZ6kQ>(J-vaY{^o6L3adN&DE+7w&ligc0`SfW9=)rMyRFlpL?0WAK z(@ehpLRKil_LX)80iayQmafa%7LUp^Cmh-k@$uCO_@Yr!QAgOc&PZSl9zTvQo`^nw zZOtg)L%g&30`Sm7_@(etM{SmZP?loA^7ePGdZ5aRidy?|zV=wVc*J`i!vE9DsfPo6 zo04Nc1%}}DeB@x;x|*7pdWKR;2x3)3!<;tceSCS`t2{1u-m1a<33!2hy}PnEcH(-UK_8LB74eO2BrpF9Y$1RojIU z&au=OjvfUGNp=PH@^v0-|H?Ol?hzBP^Z^;SVdnE$1H6HmPC;CSaW%#w&e~DYNg}5d zuh!l~oP7h*V`=j_|Jt1*%g5(@7oT_r#OOK+hah>Y`9c53VvQx3gK=YW_TOm=b>d-= zM>_#JUWcVXBeAw-C1-pFy3?6A%uGxbt~>dSE=Q}999+CMPioNaXC8<;B=GP6ASrKN zUkysKY07v>39D^x5mCx^PEE~ZVE#tty0_Y0kH653tk#n3Y?@zCU3)qr5hXpY z<)W>?xSFQv`9R=s=CTvj*3cDIexao?TBn1{>|fKBBFHwSg8xv0(Z{wlHs%inw46fF zo@%p)9Rz71zd^I?kM1rUEgZI7T+02!clwLI+uR0}!%O0yDi>R@r4XKF)U)d}yE}aR zh>wwiGYuS=9len*uC6ChAmTAj%Ji5+E~05T@86dk0AZ1H2Bk$8bOn=aSNiUR9+uPr zs@WH5xpJ!1XggUV(FB@?$XTfAtjdkmAqd~l&Oz8S_~Dm=9b3^WU^}eE2*r|QfTtp= z3FK6(CMc`^zc#KtAf|1Nr^23!#>k@+O*A{pKDZ6Wm zV&r%vWK`mokZwbS(F3`jM`*_BF%{)b3WuDc``ga_^ZvVM&Dwi?>sxDm>-&DcUmkzO zlzVHRHa8EIrF?5B$i}gdRtC1@W~~hNZHt9f*Y)4LwCTvO999fCj*C43*@Qt)tCrRE^Gm9oQ9JOQXM|J(+!Dsww$3ACi@(U$Hn-DM>ueqtUm#?35#p8`jkm+OrZ(W`|{nuh1Y^R?tjvG(^5UwKzn-uzMSLO zqxGbzi6M}g%{07>07j#<7KGzndWbg8`k%->dUW=%U7zou@61Uevs--rI5;V4K|VB`a2pFYaKav#}Q~ zWBoDy`RLy-xFNwiolDK4F9K=v6Jj3_tpNj-=4(0*|5o_PPm+J8ivKgJ7n5C*u&vbd zybTNtP_Bl_E=q-GcQrOPrVOgN!<(Ph<<+fAyuA-g6d3N5B8loWebjHTA4VJY*DX zF@ER!@*FQvA$)xsR)!@RD!WhyPvXNjodl9IvBvP{RU-9DvWV+#feaBsHf!ja`oOiY}<#g1m*bI(>rc0N4u>p_1w?c&9cARBGncmy-Y>EmO+!_w)0vNWbfbUkWfukms;Ch{ym*FUYl+kb)xf z*3UUezcJ-Lx)1d(7X(sZb9xfe+b}GBza5AozqKzfN9^6uNRazzWf(kpk#-Ogtvh#C z2L=;su;%%JP5|V_eINV^#MeZUCP?&JGSk(o3(5)1a&?U3nZ2vgBem0L_ipTs zmJBv{OlmY4AD{g71C*8|Z!w>0z)I?D5r9vBHRwk$KAn8$$k!~Kys)4hMU0soq58fg zAOQDL9W}#24h!`n=IoUU2Rt@y>O$HiEAq-2Odv1QXAn%#FQ91GnP@Z`NE+}dV$I%3 zm9Fa}BEEeecwc7%SaI&`>9iyR58K=Og;SG5LYDm9+3qh3yWt`;eD%n@{Nft!_5H(9 zZb|_R3f~!o*|rj>Op9dGlZ|#V}43*Q}%0A%^GVq=aM^v@2ZP>b5!`ICA983!VWb0i#%> z1C(T)4v;qp(rqcC)xdZ@x7Wd}prqtMnWiQ zF7#58eG7n}0dfXxgN-okxJ)1C)~L@}Usf{)+Bd0knrc+nGdDMq2A#AAL6GoLM^Ic` zd41_anr)md<9s=gkmnGisJVw22s#4M=)*b|EoaV*pHpDtS z@bPR+#jpiaXOYvcB9E4llbSkgIVQkVDy%BN%D}qi`GXgFMUcT(Ov&^KIS6cPTYba7 z6!1c_b-M?^kzm3s%8TE=ee)Y#k`@{LCMT_|8fpoKQwplX3pj**yIfrax-pc4cso^v zRunNuhb2mSaNOR;)wOv+AcSfLf1En(lL3JoE{ZoA{f?AYUAX=5XLlN+Exp4x<@dv#cq$V{w)M!~NkKwzlX8fCI60j5-f z%N|yhNoDwRu1gYQTSE)grPF4KsGg})n^T5f<1-f*I~X&HO^|xpl9Z&97eWE9HZ~Be zb(CYzM6)U=o$*aKMn{V-ZGrxqz$06kjgoWHfTps}1gp%GAO}4ZTkDLXU^JRIEc*l+?m3$4}?0P6h>3&~&>QuDzuE-YzMC5LI%QX|ZT*SCH`fdSU z1wZchu{-=&@)n*P5gCb7C%3-~3wPjiDddV2VeRsDLO`pakM!eeW}|Zp>T~9to|hpv zc2s~=QnSD4sy@j{rJd{u5pJf}?_8Q-y{h0?4%V2+6nl>!X!je6N`#X>VPOdIizAsh~QZ|3m?9sDdW50oZ#{R_hVPVas z!IcP{qA;rHc0DzcY$L~w8|R;0a{hc<_}{%u=Hfo8L`?tS4$x(ru)WUlma8}5*r@jT zWyOla@TdPh>jWej^kut|xQsx%!WiOu2An8+9D!hA+clku>wM5B&xGY)VhDGTa}|rl zII1S}J%PU1PiQ@n6LkdcZ{F6n3|#kBG65l7VxgLqh&TXqQVyZDJyj2*At9(>v;&QA zmLdO?_^}B40S#Av1Pu#_r$i3U{jrM)%%wtc8ypyg$7Ms-mX-;BeL9Dx0_=c{;1_P? zXTST_E)CX!p|fbE6@>Zz0(iaj)WREF*PUB1C@FS*#w0j^!1i3V+=r0rcA~y++-iLO z5E2aYw{;&vkGkNvQ4+pKK*s+U6@pCo@gyx;4ip9g{S=&{>jWf diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index b93603f5a6..3f6cb9c54b 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -18,6 +18,8 @@ @import "tailwindcss/theme.css" layer(theme); @import "./preflight.css" layer(base); @import "tailwindcss/utilities.css" layer(utilities); + +@source "../src"; @import "katex/dist/katex.min.css"; @plugin "tailwindcss-animate"; From cd29243b7d7a5ad8d224eb4b1a9c4aec36f25494 Mon Sep 17 00:00:00 2001 From: Ivan Ramadhan Arifin <111653938+ivanarifin@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:29:14 +0700 Subject: [PATCH 80/89] fix(openai-compatible): add max reasoning effort option (#882) (#1051) --- src/api/providers/__tests__/openai.spec.ts | 21 ++++++++++ .../settings/providers/OpenAICompatible.tsx | 6 +-- .../__tests__/OpenAICompatible.spec.tsx | 42 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 9c79e56159..72d219e02a 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -453,6 +453,27 @@ describe("OpenAiHandler", () => { expect(callArgs.reasoning_effort).toBe("high") }) + it("should pass through max reasoning_effort when configured by an OpenAI-compatible model", async () => { + const reasoningOptions: ApiHandlerOptions = { + ...mockOptions, + enableReasoningEffort: true, + openAiCustomModelInfo: { + contextWindow: 128_000, + supportsPromptCache: false, + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], + reasoningEffort: "max", + }, + } + const reasoningHandler = new OpenAiHandler(reasoningOptions) + const stream = reasoningHandler.createMessage(systemPrompt, messages) + for await (const _chunk of stream) { + } + + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("max") + }) + it("should not include reasoning_effort when reasoning effort is disabled", async () => { const noReasoningOptions: ApiHandlerOptions = { ...mockOptions, diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 0524932c5f..8b11c128c7 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -6,7 +6,7 @@ import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { type ProviderSettings, type ModelInfo, - type ReasoningEffort, + type ReasoningEffortExtended, type OrganizationAllowList, type ExtensionMessage, azureOpenAiDefaultApiVersion, @@ -266,13 +266,13 @@ export const OpenAICompatible = ({ setApiConfigurationField("openAiCustomModelInfo", { ...openAiCustomModelInfo, - reasoningEffort: value as ReasoningEffort, + reasoningEffort: value as ReasoningEffortExtended, }) } }} modelInfo={{ ...(apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults), - supportsReasoningEffort: ["low", "medium", "high", "xhigh"], + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], }} /> )} diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index aba81ec219..3da109efc2 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -76,8 +76,13 @@ vi.mock("../../R1FormatSetting", () => ({ R1FormatSetting: () =>
R1 Format Setting
, })) +const { mockThinkingBudget } = vi.hoisted(() => ({ mockThinkingBudget: vi.fn() })) + vi.mock("../../ThinkingBudget", () => ({ - ThinkingBudget: () =>
Thinking Budget
, + ThinkingBudget: (props: any) => { + mockThinkingBudget(props) + return
Thinking Budget
+ }, })) // Mock react-use @@ -312,4 +317,39 @@ describe("OpenAICompatible Component - includeMaxTokens checkbox", () => { expect(description).toHaveClass("text-sm", "text-vscode-descriptionForeground", "ml-6") }) }) + describe("reasoning effort", () => { + it("should expose and persist the max reasoning-effort value", () => { + const apiConfiguration: Partial = { + enableReasoningEffort: true, + openAiCustomModelInfo: { + contextWindow: 128_000, + supportsPromptCache: false, + }, + } + + render( + , + ) + + const thinkingBudgetProps = mockThinkingBudget.mock.calls[0][0] + expect(thinkingBudgetProps.modelInfo.supportsReasoningEffort).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + ]) + + thinkingBudgetProps.setApiConfigurationField("reasoningEffort", "max") + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("openAiCustomModelInfo", { + ...apiConfiguration.openAiCustomModelInfo, + reasoningEffort: "max", + }) + }) + }) }) From 5971caacb193bc05b19de8133aca07cf30d7fc41 Mon Sep 17 00:00:00 2001 From: Alexei Gubin <36731953+WebMad@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:03:27 +0300 Subject: [PATCH 81/89] feat(openai-codex): persist and send Fast priority mode (#1063) * feat(openai-codex): persist and send Fast priority mode * refactor(openai-codex): align service tier types --- .../src/__tests__/provider-settings.test.ts | 47 +++++++- packages/types/src/model.ts | 12 ++ packages/types/src/provider-settings.ts | 12 +- .../providers/__tests__/openai-codex.spec.ts | 113 ++++++++++++++++++ src/api/providers/openai-codex.ts | 15 +++ .../__tests__/ProviderSettingsManager.spec.ts | 33 ++++- 6 files changed, 228 insertions(+), 4 deletions(-) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 6b61f6fd9f..cd786a6529 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,4 +1,49 @@ -import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js" +import { ANTHROPIC_API_PROTOCOL, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js" +import { + getApiProtocol, + OPEN_AI_CODEX_SERVICE_TIER_KEY, + PROVIDER_SETTINGS_KEYS, + providerSettingsSchema, + providerSettingsSchemaDiscriminated, +} from "../provider-settings.js" +import { OpenAiCodexServiceTier, OpenAiServiceTier } from "../model.js" + +describe("OpenAI Codex provider settings", () => { + it("preserves the Fast preference in general and provider-specific schemas", () => { + const settings = { + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + } + + expect(providerSettingsSchema.parse(settings)).toEqual(settings) + expect(providerSettingsSchemaDiscriminated.parse(settings)).toEqual(settings) + expect(PROVIDER_SETTINGS_KEYS).toContain(OPEN_AI_CODEX_SERVICE_TIER_KEY) + }) + + it.each([undefined, OpenAiCodexServiceTier.Default])( + "accepts %s as the Standard preference", + (openAiCodexServiceTier) => { + const standardSettings = { + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + ...(openAiCodexServiceTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier } : {}), + } + + expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings) + }, + ) + + it("rejects unsupported service tiers", () => { + expect( + providerSettingsSchemaDiscriminated.safeParse({ + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiServiceTier.Flex, + }).success, + ).toBe(false) + }) +}) describe("getApiProtocol", () => { it("preserves API protocol wire values", () => { diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 28a32eecb4..9fbf9e358b 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -70,6 +70,18 @@ export const serviceTiers = [OpenAiServiceTier.Default, OpenAiServiceTier.Flex, export const serviceTierSchema = z.enum(serviceTiers) export type ServiceTier = z.infer +/** + * Service tiers for Codex requests authenticated through a ChatGPT subscription. + */ +export const OpenAiCodexServiceTier = { + Default: "default", + Priority: "priority", +} as const + +export const openAiCodexServiceTiers = [OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority] as const +export const openAiCodexServiceTierSchema = z.enum(openAiCodexServiceTiers) +export type OpenAiCodexServiceTier = z.infer + /** * ModelParameter */ diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c2286c1dfd..e17cd5ddbc 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -1,6 +1,12 @@ import { z } from "zod" -import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js" +import { + modelInfoSchema, + openAiCodexServiceTierSchema, + reasoningEffortSettingSchema, + verbosityLevelsSchema, + serviceTierSchema, +} from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" import { providerIdentifiers, @@ -38,6 +44,7 @@ import { */ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 +export const OPEN_AI_CODEX_SERVICE_TIER_KEY = "openAiCodexServiceTier" /** * DynamicProvider @@ -279,7 +286,8 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ }) const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ - // No additional settings needed - uses OAuth authentication + // Codex "Fast" mode maps to the Responses API priority service tier. + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTierSchema.optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index c4249f4183..dbe13d576b 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -9,6 +9,7 @@ vitest.mock("@roo-code/telemetry", () => ({ })) import { Anthropic } from "@anthropic-ai/sdk" +import { OPEN_AI_CODEX_SERVICE_TIER_KEY, OpenAiCodexServiceTier, SERVICE_TIER_KEY } from "@roo-code/types" import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" @@ -76,6 +77,84 @@ describe("OpenAiCodexHandler.getModel", () => { }) describe("OpenAiCodexHandler.createMessage", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + it("sends the priority service tier in streaming SDK requests when Fast is selected", async () => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + Reflect.set(handler, "client", { responses: { create: mockCreate } }) + + await drainStream(handler.createMessage("System prompt", [])) + + const [body] = mockCreate.mock.calls[0] + expect(body).toMatchObject({ + stream: true, + [SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + }) + }) + + it.each([ + ["an absent preference", {}], + [ + "an explicit Standard preference from an older profile", + { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Default }, + ], + ])("omits the service tier in streaming SDK requests for %s", async (_description, serviceTierOptions) => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + ...serviceTierOptions, + } as ConstructorParameters[0]) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + Reflect.set(handler, "client", { responses: { create: mockCreate } }) + + await drainStream(handler.createMessage("System prompt", [])) + + expect(mockCreate.mock.calls[0][0]).not.toHaveProperty(SERVICE_TIER_KEY) + }) + + it("preserves the priority service tier in the manual streaming fallback", async () => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + Reflect.set(handler, "client", { + responses: { create: vitest.fn().mockRejectedValue(new Error("SDK unavailable")) }, + }) + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.completed","response":{"output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n', + ), + ) + controller.close() + }, + }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await drainStream(handler.createMessage("System prompt", [])) + + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({ + stream: true, + [SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + }) + }) + it("should skip URL-sourced images in formatFullConversation (only base64 emits input_image)", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" }) @@ -176,6 +255,40 @@ describe("OpenAiCodexHandler.createMessage", () => { }) }) +describe("OpenAiCodexHandler.completePrompt service tier", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + it.each<[string, OpenAiCodexServiceTier | undefined, typeof OpenAiCodexServiceTier.Priority | undefined]>([ + ["Fast", OpenAiCodexServiceTier.Priority, OpenAiCodexServiceTier.Priority], + ["Standard", undefined, undefined], + ])("uses the %s preference in non-streaming requests", async (_mode, configuredTier, expectedTier) => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + ...(configuredTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: configuredTier } : {}), + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + json: vitest.fn().mockResolvedValue({ text: "Complete" }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await expect(handler.completePrompt("Hello")).resolves.toBe("Complete") + + const body = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(body.stream).toBe(false) + if (expectedTier) { + expect(body[SERVICE_TIER_KEY]).toBe(expectedTier) + } else { + expect(body).not.toHaveProperty(SERVICE_TIER_KEY) + } + }) +}) + describe("transformLunaResponsesLiteBody", () => { it("creates the exact Responses Lite body while preserving unrelated fields and reasoning", () => { const tools = [{ type: "function", name: "read_file", parameters: { type: "object" } }] diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 27ddbe19e2..e9bc3bbf5d 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -5,9 +5,12 @@ import OpenAI from "openai" import { type ModelInfo, + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, openAiCodexDefaultModelId, OpenAiCodexModelId, openAiCodexModels, + SERVICE_TIER_KEY, type ReasoningEffort, type ReasoningEffortExtended, ApiProviderError, @@ -29,6 +32,8 @@ import { t } from "../../i18n" export type OpenAiCodexModel = ReturnType +type OpenAiCodexRequestServiceTier = typeof OpenAiCodexServiceTier.Priority + /** * OpenAI Codex base URL for API requests * Per the implementation guide: requests are routed to chatgpt.com/backend-api/codex @@ -37,6 +42,11 @@ const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex" const LUNA_MODEL_ID = "gpt-5.6-luna" const LUNA_CODEX_VERSION = "0.144.0" +const getOpenAiCodexServiceTier = (options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined => + options[OPEN_AI_CODEX_SERVICE_TIER_KEY] === OpenAiCodexServiceTier.Priority + ? OpenAiCodexServiceTier.Priority + : undefined + function stripInputImageDetail(value: any): any { if (Array.isArray(value)) { return value.map(stripInputImageDetail) @@ -365,6 +375,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion model: string input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }> stream: boolean + [SERVICE_TIER_KEY]?: OpenAiCodexRequestServiceTier reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" } temperature?: number store?: boolean @@ -383,12 +394,14 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Per the implementation guide: Codex backend may reject max_output_tokens // and prompt_cache_retention, so we omit them + const serviceTier = getOpenAiCodexServiceTier(this.options) const body: ResponsesRequestBody = { model: model.id, input: formattedInput, stream: true, store: false, instructions: systemPrompt, + ...(serviceTier ? { [SERVICE_TIER_KEY]: serviceTier } : {}), // Only include encrypted reasoning content when reasoning effort is set ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), ...(reasoningEffort @@ -1261,6 +1274,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } const reasoningEffort = this.getReasoningEffort(model) + const serviceTier = getOpenAiCodexServiceTier(this.options) const baseRequestBody: any = { model: model.id, @@ -1272,6 +1286,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion ], stream: false, store: false, + ...(serviceTier ? { [SERVICE_TIER_KEY]: serviceTier } : {}), ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), } diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index c6bd19c0b1..491ef3b18a 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -2,7 +2,12 @@ import { ExtensionContext } from "vscode" -import type { ProviderSettings } from "@roo-code/types" +import { + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, + providerIdentifiers, + type ProviderSettings, +} from "@roo-code/types" import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager" @@ -451,6 +456,32 @@ describe("ProviderSettingsManager", () => { expect(storedConfig).toEqual(expectedConfig) }) + it.each([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority] as const)( + "should persist the OpenAI Codex %s speed preference", + async (openAiCodexServiceTier) => { + mockSecrets.get.mockResolvedValue( + JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { default: {} }, + modeApiConfigs: {}, + }), + ) + + await providerSettingsManager.saveConfig("codex", { + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier, + }) + + const storedProfiles = JSON.parse(mockSecrets.store.mock.calls.at(-1)?.[1]) + expect(storedProfiles.apiConfigs.codex).toMatchObject({ + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier, + }) + }, + ) + it("should only save provider relevant settings", async () => { mockSecrets.get.mockResolvedValue( JSON.stringify({ From eace9674f33a6a0a5a8ac59846dd29a7aabe4390 Mon Sep 17 00:00:00 2001 From: "zoomote[bot]" <305051434+zoomote[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:53:08 +0000 Subject: [PATCH 82/89] [Fix] Subtask e2e suite can inherit a cancelled delayed mock stream from the previous test (#1074) * test(e2e): drain delayed mock stream deterministically in subtask suite The API-hang subtask fixture used aimock's flat latency, which applies per SSE chunk and is never interrupted by client disconnects, so a cancelled delayed stream stayed pending server-side for chunks x latency and could flush into the next test's traffic. - delay only the first chunk via streamingProfile.ttft so the pending window is exactly the shared SUBTASK_API_HANG_RESPONSE_LATENCY_MS - anchor the post-test drain to the request's aimock journal timestamp and wait out only the remainder of that bounded window * docs(e2e): clarify subtask drain invariants from code review --------- Co-authored-by: Roomote Co-authored-by: Elliott de Launay --- apps/vscode-e2e/AGENTS.md | 11 +++ apps/vscode-e2e/src/fixtures/subtasks.ts | 16 ++++- apps/vscode-e2e/src/suite/subtasks.test.ts | 80 ++++++++++++++++++---- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/apps/vscode-e2e/AGENTS.md b/apps/vscode-e2e/AGENTS.md index 8d9a4defac..ccd4c4910e 100644 --- a/apps/vscode-e2e/AGENTS.md +++ b/apps/vscode-e2e/AGENTS.md @@ -121,6 +121,17 @@ Example: The `model` field can be added to either match when a test targets a specific model. +## Delaying a fixture response (simulating a slow or hung provider) + +Use `streamingProfile: { ttft: }` on a fixture, not a flat `latency: `, when a test needs +to simulate a slow or hung provider (e.g. to cancel an in-flight request mid-stream). `ttft` delays +only the first SSE chunk, so the pending window is exactly the configured value. Flat `latency` +delays _every_ chunk, and aimock never observes client disconnects — after a test cancels the +request, a flat-latency stream keeps flushing chunks server-side for `chunks × latency` before +reaching the dead socket, which can interleave with the next test's request against the same mock +server. See `SUBTASK_API_HANG_RESPONSE_LATENCY_MS` in `fixtures/subtasks.ts` for an example, +including the bounded post-test drain the calling suite uses to wait out that window. + ## 404 errors in logs are expected Background API calls from the extension (usage collection, initialization) hit aimock with no matching fixture and return 404. These do **not** affect test results — the tests still pass. You'll see `[OpenRouter] API error: { message: '404 No fixture matched' }` in the output; this is normal. diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index f5dfc813df..30d3852b0a 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -33,6 +33,12 @@ export const SUBTASK_API_HANG_RESUME_MESSAGE = "Continue after provider hang." export const SUBTASK_API_HANG_CHILD_RESULT = "Hung child completed" export const SUBTASK_API_HANG_PARENT_RESULT = "API hang parent resumed" +// How long the API-hang child's first mocked response stays pending before its first SSE +// chunk. Shared with the subtask suite so its post-test drain waits exactly one window. +// Correctness depends on no flat `latency` (fixture or LLMock default) being set on that +// fixture — a flat latency would apply to every chunk after the first, not just the ttft. +export const SUBTASK_API_HANG_RESPONSE_LATENCY_MS = 15_000 + // Abandon-subtask scenario (#559) — separate markers to avoid sequenceIndex collisions with the // interrupted-child-resumes tests above, which exhaust the sequence count for INTERRUPT markers. const SUBTASK_ABANDON_PARENT_MARKER = "SUBTASK_PARENT_ABANDON_SEVER" @@ -261,8 +267,14 @@ export function addSubtaskFixtures(mock: InstanceType) { userMessage: apiHangChildMatch, sequenceIndex: 0, }, - // Keep the first child response pending long enough for the e2e test to cancel an in-flight API request. - latency: 15_000, + // Keep the first child response pending long enough for the e2e test to cancel an in-flight + // API request. Delay only the first chunk (ttft) rather than using flat `latency`: aimock + // applies `latency` to EVERY chunk and never observes client disconnects, so after the test + // cancels, a flat-latency stream would stay pending server-side for chunks × latency before + // flushing to the dead socket. With ttft the pending window is exactly + // SUBTASK_API_HANG_RESPONSE_LATENCY_MS (see its doc comment for the no-flat-latency + // invariant this relies on), which is what the suite's post-test drain waits out. + streamingProfile: { ttft: SUBTASK_API_HANG_RESPONSE_LATENCY_MS }, response: { toolCalls: [ { diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 9e1d0e83cb..0c0d410f90 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -16,6 +16,7 @@ import { SUBTASK_API_HANG_PARENT_MARKER, SUBTASK_API_HANG_PARENT_PROMPT, SUBTASK_API_HANG_PARENT_RESULT, + SUBTASK_API_HANG_RESPONSE_LATENCY_MS, SUBTASK_API_HANG_RESUME_MESSAGE, SUBTASK_CHILD_FOLLOWUP_ANSWER, SUBTASK_FAST_CHILD_RESULT, @@ -33,6 +34,7 @@ import { type AimockMessageContent = string | Array<{ type?: string; text?: string }> type AimockJournalEntry = { + timestamp?: number body?: { messages?: Array<{ role?: string @@ -49,24 +51,67 @@ const messageContentText = (content?: AimockMessageContent) => { return content?.map((part) => part.text ?? "").join("") ?? "" } -const waitForAimockRequestContaining = async (expectedText: string, excludeText?: string) => { +const fetchAimockJournal = async () => { const aimockUrl = process.env.AIMOCK_URL assert.ok(aimockUrl, "AIMOCK_URL must be set for aimock journal assertions") + const response = await fetch(`${aimockUrl}/__aimock/journal`) + return (await response.json()) as AimockJournalEntry[] +} + +const findAimockRequest = (entries: AimockJournalEntry[], expectedText: string, excludeText?: string) => + entries.find((entry) => { + const messages = entry.body?.messages + if (!messages) return false + const entryText = messages.map((m) => messageContentText(m.content)).join("") + if (excludeText && entryText.includes(excludeText)) return false + return messages.some( + (message) => message.role === "user" && messageContentText(message.content).includes(expectedText), + ) + }) + +// Waits for a matching request to appear in the aimock journal and returns its journal +// timestamp, so callers can anchor post-test drains to the exact request this test created. +const waitForAimockRequestContaining = async ( + expectedText: string, + excludeText?: string, +): Promise => { + let matchedAt: number | undefined + await waitFor(async () => { - const response = await fetch(`${aimockUrl}/__aimock/journal`) - const entries = (await response.json()) as AimockJournalEntry[] - - return entries.some((entry) => { - const messages = entry.body?.messages - if (!messages) return false - const entryText = messages.map((m) => messageContentText(m.content)).join("") - if (excludeText && entryText.includes(excludeText)) return false - return messages.some( - (message) => message.role === "user" && messageContentText(message.content).includes(expectedText), - ) - }) + matchedAt = findAimockRequest(await fetchAimockJournal(), expectedText, excludeText)?.timestamp + return matchedAt !== undefined }) + + return matchedAt +} + +// Grace period after the delayed window for aimock to flush the stream's remaining chunks to +// the dead socket. 500ms is an empirical margin for that flush plus socket teardown; if this +// suite becomes flaky again on slow CI runners, widen this value first. +const SUBTASK_API_HANG_DRAIN_GRACE_MS = 500 + +// aimock does not observe client disconnects: after the API-hang child request is cancelled, +// the mock keeps the delayed stream pending server-side until the fixture's ttft has fully +// elapsed, then flushes the remaining chunks to the dead socket. A streamed request opened by +// the next test can interleave with that late flush, so wait out the remainder of the delayed +// window before the next test runs. The deadline is anchored to the journal timestamp of the +// request this test created (never earlier traffic), and bounded by one latency window plus +// grace, so it cannot hide a genuine hang. +const waitForDelayedSubtaskStreamDrain = async (delayedRequestStartedAt: number | undefined) => { + if (delayedRequestStartedAt === undefined) { + // The delayed request never reached the mock (the test failed before cancelling + // an in-flight request), so there is no delayed stream to drain. + return + } + + const drainDeadlineMs = + delayedRequestStartedAt + SUBTASK_API_HANG_RESPONSE_LATENCY_MS + SUBTASK_API_HANG_DRAIN_GRACE_MS + const remainingMs = drainDeadlineMs - Date.now() + + if (remainingMs > 0) { + await sleep(remainingMs) + } } suite("Roo Code Subtasks", function () { @@ -482,6 +527,7 @@ suite("Roo Code Subtasks", function () { const api = globalThis.api const asks: Record = {} const says: Record = {} + let delayedChildRequestStartedAt: number | undefined const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { if (message.type === "ask") { @@ -519,7 +565,10 @@ suite("Roo Code Subtasks", function () { return false }) - await waitForAimockRequestContaining(SUBTASK_API_HANG_CHILD_MARKER, SUBTASK_API_HANG_PARENT_MARKER) + delayedChildRequestStartedAt = await waitForAimockRequestContaining( + SUBTASK_API_HANG_CHILD_MARKER, + SUBTASK_API_HANG_PARENT_MARKER, + ) await api.cancelCurrentTask() @@ -580,6 +629,9 @@ suite("Roo Code Subtasks", function () { await api.clearCurrentTask() } await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) + // Drain the cancelled delayed stream before the next test can open another + // streamed request against the mock. + await waitForDelayedSubtaskStreamDrain(delayedChildRequestStartedAt) } }) From ca9b60fbc7b72a27352b0e2084271c28f2419f94 Mon Sep 17 00:00:00 2001 From: James Mtendamema <59908268+JamesRobert20@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:55:08 +0300 Subject: [PATCH 83/89] fix(router-provider): fetch model metadata before context management decisions (#1053) * fix(router-provider): fetch model metadata before context management decisions Router providers (zoo-gateway, kimi-code) that are auth-scoped skip the model cache entirely. On a fresh handler instance getModel() falls back to hardcoded defaults (e.g. 200k context window) because the real model list has not been fetched yet. Context management runs before createMessage() which is where fetchModel() normally happens, so condensing/truncation decisions use the wrong context window. Add ensureModelFetched() to RouterProvider that fetches once when the instance model map is empty. Call it in Task before context management so getModel() returns accurate metadata from the API. Co-authored-by: Cursor * fix(router-provider): single-flight ensureModelFetched and earlier call site Make ensureModelFetched single-flight so concurrent callers share a single in-flight fetch instead of firing duplicates. Move the call site before the cachedStreamingModel snapshot so the model info is accurate from the start of the streaming session, not just for context management. Co-authored-by: Cursor * fix(router-provider): address review feedback on fetch failures and double-fetch Make fetchModel single-flight and short-circuit once models are loaded so auth-scoped providers do not hit the models endpoint twice per request. Catch ensureModelFetched failures in Task via safeEnsureModelFetched so a metadata fetch error falls back to defaults instead of ending the task. Add reject-then-recover coverage and Task tests for the new call sites. Co-authored-by: Cursor * fix(test): spy private addToApiConversationHistory via TaskTestAccess vi.spyOn on the private method fails check-types; route it through the existing test access cast like the other private helpers. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- src/api/index.ts | 7 + .../providers/__tests__/zoo-gateway.spec.ts | 88 +++++++ src/api/providers/router-provider.ts | 28 ++- src/core/task/Task.ts | 20 ++ src/core/task/__tests__/Task.spec.ts | 234 ++++++++++++++++++ 5 files changed, 375 insertions(+), 2 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index c9992c9c39..f48ab50c0e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -125,6 +125,13 @@ export interface ApiHandler { getModel(): { id: string; info: ModelInfo } + /** + * Ensures model metadata has been fetched from the remote API so that getModel() + * returns accurate info (context window, pricing, etc.) instead of hardcoded defaults. + * Only router providers that discover models over the network implement this. + */ + ensureModelFetched?(): Promise + /** * Optional context window for context-management / auto-condense when it must differ from * getModel().info.contextWindow. Only VS Code LM overrides it (static `maxInputTokens` vs its diff --git a/src/api/providers/__tests__/zoo-gateway.spec.ts b/src/api/providers/__tests__/zoo-gateway.spec.ts index e0c060db3b..e797dc9745 100644 --- a/src/api/providers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/__tests__/zoo-gateway.spec.ts @@ -635,4 +635,92 @@ describe("ZooGatewayHandler", () => { ) }) }) + + describe("ensureModelFetched", () => { + it("fetches models when instance models are empty", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const { getModels } = await import("../fetchers/modelCache") + + expect(handler.getModel().info.contextWindow).toBe(200000) + + await handler.ensureModelFetched() + + expect(getModels).toHaveBeenCalled() + }) + + it("skips the fetch when models are already populated", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const { getModels } = await import("../fetchers/modelCache") + + await handler.ensureModelFetched() + vitest.mocked(getModels).mockClear() + + await handler.ensureModelFetched() + expect(getModels).not.toHaveBeenCalled() + }) + + it("short-circuits a subsequent fetchModel call after models are populated", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const { getModels } = await import("../fetchers/modelCache") + + await handler.ensureModelFetched() + vitest.mocked(getModels).mockClear() + + await handler.fetchModel() + expect(getModels).not.toHaveBeenCalled() + }) + + it("deduplicates concurrent calls into a single fetch", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const { getModels } = await import("../fetchers/modelCache") + vitest.mocked(getModels).mockClear() + + await Promise.all([handler.ensureModelFetched(), handler.ensureModelFetched()]) + + expect(getModels).toHaveBeenCalledTimes(1) + }) + + it("recovers after a rejected fetch so later calls are not poisoned", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const { getModels } = await import("../fetchers/modelCache") + + vitest.mocked(getModels).mockRejectedValueOnce(new Error("network down")) + await expect(handler.ensureModelFetched()).rejects.toThrow("network down") + + vitest.mocked(getModels).mockResolvedValueOnce({ + "anthropic/claude-sonnet-4": { + maxTokens: 64000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + }, + }) + await handler.ensureModelFetched() + + expect(handler.getModel().info.contextWindow).toBe(1000000) + }) + + it("makes getModel return the fetched context window instead of the default", async () => { + const { getModels } = await import("../fetchers/modelCache") + vitest.mocked(getModels).mockResolvedValueOnce({ + "google/gemini-2.5-pro": { + maxTokens: 65536, + contextWindow: 1048576, + supportsImages: true, + supportsPromptCache: false, + }, + }) + + const handler = new ZooGatewayHandler({ + ...mockOptions, + zooGatewayModelId: "google/gemini-2.5-pro", + }) + + expect(handler.getModel().info.contextWindow).toBe(200000) + + await handler.ensureModelFetched() + + expect(handler.getModel().info.contextWindow).toBe(1048576) + }) + }) }) diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index 7a983ed12e..cbdd49e58b 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -56,9 +56,33 @@ export abstract class RouterProvider extends BaseProvider { }) } + private modelFetchPromise?: Promise<{ id: string; info: ModelInfo }> + public async fetchModel() { - this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey, baseUrl: this.client.baseURL }) - return this.getModel() + if (Object.keys(this.models).length > 0) { + return this.getModel() + } + + if (!this.modelFetchPromise) { + this.modelFetchPromise = getModels({ + provider: this.name, + apiKey: this.client.apiKey, + baseUrl: this.client.baseURL, + }) + .then((models) => { + this.models = models + return this.getModel() + }) + .finally(() => { + this.modelFetchPromise = undefined + }) + } + + return this.modelFetchPromise + } + + async ensureModelFetched(): Promise { + await this.fetchModel() } override getModel(): { id: string; info: ModelInfo } { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8f69a3a0d4..4ba2996c91 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2762,6 +2762,8 @@ export class Task extends EventEmitter implements TaskLike { await this.diffViewProvider.reset() + await this.safeEnsureModelFetched() + // Cache model info once per API request to avoid repeated calls during streaming // This is especially important for tools and background usage collection this.cachedStreamingModel = this.api.getModel() @@ -3837,11 +3839,28 @@ export class Task extends EventEmitter implements TaskLike { ) } + /** + * Ensures router-provider model metadata is loaded before getModel() is used for + * context management or streaming. Failures fall back to hardcoded defaults rather + * than aborting the task. + */ + private async safeEnsureModelFetched(): Promise { + try { + await this.api.ensureModelFetched?.() + } catch (error) { + console.error( + `[Task#${this.taskId}] Failed to fetch model metadata:`, + error instanceof Error ? error.message : error, + ) + } + } + private async handleContextWindowExceededError(): Promise { const state = await this.providerRef.deref()?.getState() const { profileThresholds = {}, mode, apiConfiguration } = state ?? {} const { contextTokens } = this.getTokenUsage() + await this.safeEnsureModelFetched() const modelInfo = this.api.getModel().info const maxTokens = getModelMaxOutputTokens({ @@ -4042,6 +4061,7 @@ export class Task extends EventEmitter implements TaskLike { const { contextTokens } = this.getTokenUsage() if (contextTokens) { + await this.safeEnsureModelFetched() const modelInfo = this.api.getModel().info const maxTokens = getModelMaxOutputTokens({ diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index d9f7240c5c..330241e221 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -32,6 +32,8 @@ type TaskTestAccess = { presentAssistantMessageSafe: () => void updateClineMessage: (message: import("@roo-code/types").ClineMessage) => Promise saveClineMessages: () => Promise + safeEnsureModelFetched: () => Promise + addToApiConversationHistory: (message: unknown, reasoning?: string) => Promise } function getTaskTestAccess(task: Task): TaskTestAccess { @@ -185,6 +187,14 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({ getEnvironmentDetails: vi.fn().mockResolvedValue(""), })) +vi.mock("../../mentions/processUserContentMentions", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + processUserContentMentions: vi.fn().mockImplementation(actual.processUserContentMentions), + } +}) + vi.mock("../../ignore/RooIgnoreController") vi.mock("../../../i18n", () => { @@ -2507,6 +2517,230 @@ describe("Cline", () => { }) }) + describe("safeEnsureModelFetched", () => { + it("loads model metadata before getModel is used", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const ensureModelFetched = vi.fn().mockResolvedValue(undefined) + Object.assign(task.api, { ensureModelFetched }) + + await getTaskTestAccess(task).safeEnsureModelFetched() + + expect(ensureModelFetched).toHaveBeenCalledTimes(1) + }) + + it("swallows fetch failures so callers can fall back to defaults", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const ensureModelFetched = vi.fn().mockRejectedValue(new Error("network down")) + Object.assign(task.api, { ensureModelFetched }) + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + await expect(getTaskTestAccess(task).safeEnsureModelFetched()).resolves.toBeUndefined() + + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to fetch model metadata"), + "network down", + ) + errorSpy.mockRestore() + }) + + it("is a no-op when the api handler does not implement ensureModelFetched", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + await expect(getTaskTestAccess(task).safeEnsureModelFetched()).resolves.toBeUndefined() + }) + + it("calls safeEnsureModelFetched from attemptApiRequest when context tokens are present", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task, "getTokenUsage").mockReturnValue({ + totalCost: 0, + totalTokensIn: 0, + totalTokensOut: 0, + contextTokens: 50_000, + }) + const safeSpy = vi.spyOn(getTaskTestAccess(task), "safeEnsureModelFetched").mockResolvedValue(undefined) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200_000, + maxTokens: 4096, + } as ModelInfo, + }) + vi.spyOn(task.api, "createMessage").mockReturnValue({ + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "ok" } + }, + async next() { + return { done: true, value: undefined } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(error: unknown) { + throw error + }, + async [Symbol.asyncDispose]() {}, + } as AsyncGenerator) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] + + const iterator = task.attemptApiRequest(0) + await iterator.next() + + expect(safeSpy).toHaveBeenCalled() + }) + + it("continues attemptApiRequest when model metadata fetch fails", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task, "getTokenUsage").mockReturnValue({ + totalCost: 0, + totalTokensIn: 0, + totalTokensOut: 0, + contextTokens: 50_000, + }) + const ensureModelFetched = vi.fn().mockRejectedValue(new Error("fetch failed")) + Object.assign(task.api, { ensureModelFetched }) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200_000, + maxTokens: 4096, + } as ModelInfo, + }) + vi.spyOn(task.api, "createMessage").mockReturnValue({ + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "ok" } + }, + async next() { + return { done: false, value: { type: "text", text: "ok" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(error: unknown) { + throw error + }, + async [Symbol.asyncDispose]() {}, + } as AsyncGenerator) + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] + + const iterator = task.attemptApiRequest(0) + await expect(iterator.next()).resolves.toMatchObject({ + done: false, + value: { type: "text", text: "ok" }, + }) + expect(errorSpy).toHaveBeenCalled() + errorSpy.mockRestore() + }) + + it("fetches model metadata before caching the streaming model", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const ensureModelFetched = vi.fn().mockResolvedValue(undefined) + Object.assign(task.api, { ensureModelFetched }) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200_000, + maxTokens: 4096, + } as ModelInfo, + }) + vi.mocked(processUserContentMentions).mockResolvedValueOnce({ + content: [{ type: "text", text: "hello" }], + mode: undefined, + }) + const safeSpy = vi.spyOn(getTaskTestAccess(task), "safeEnsureModelFetched") + vi.spyOn(task, "attemptApiRequest").mockImplementation(() => { + throw new Error("stop after model metadata fetch") + }) + vi.spyOn(getTaskTestAccess(task), "saveClineMessages").mockResolvedValue(true) + vi.spyOn(task.diffViewProvider, "reset").mockResolvedValue(undefined as never) + vi.spyOn(getTaskTestAccess(task), "addToApiConversationHistory").mockResolvedValue(undefined) + + task.clineMessages = [ + { + ts: Date.now(), + type: "say", + say: "api_req_started", + text: "{}", + }, + ] + vi.spyOn(task, "say").mockImplementation(async (type) => { + if (type === "api_req_started") { + task.clineMessages.push({ + ts: Date.now(), + type: "say", + say: "api_req_started", + text: "{}", + }) + } + return undefined as never + }) + + const result = await task.recursivelyMakeClineRequests([{ type: "text", text: "hello" }], false) + + expect(result).toBe(true) + expect(safeSpy).toHaveBeenCalled() + expect(ensureModelFetched).toHaveBeenCalled() + expect(task.cachedStreamingModel?.id).toBe(mockApiConfig.apiModelId) + }) + }) + describe("start()", () => { it("should be a no-op if the task was already started in the constructor", () => { const task = new Task({ From 19bbfdf1088dfddc4a8ad5e7c3d003251b393724 Mon Sep 17 00:00:00 2001 From: Toray Altas Date: Fri, 31 Jul 2026 17:34:58 -0400 Subject: [PATCH 84/89] chore: prepare v3.74.0 release --- CHANGELOG.md | 34 +++++++++++++++++++++ README.md | 17 +++++------ locales/ca/README.md | 17 +++++------ locales/de/README.md | 17 +++++------ locales/es/README.md | 17 +++++------ locales/fr/README.md | 17 +++++------ locales/hi/README.md | 17 +++++------ locales/id/README.md | 17 +++++------ locales/it/README.md | 17 +++++------ locales/ja/README.md | 17 +++++------ locales/ko/README.md | 17 +++++------ locales/nl/README.md | 17 +++++------ locales/pl/README.md | 17 +++++------ locales/pt-BR/README.md | 17 +++++------ locales/ru/README.md | 17 +++++------ locales/tr/README.md | 17 +++++------ locales/vi/README.md | 17 +++++------ locales/zh-CN/README.md | 17 +++++------ locales/zh-TW/README.md | 17 +++++------ src/core/webview/ClineProvider.ts | 2 +- src/package.json | 2 +- webview-ui/src/i18n/locales/ca/chat.json | 6 ++-- webview-ui/src/i18n/locales/de/chat.json | 6 ++-- webview-ui/src/i18n/locales/en/chat.json | 6 ++-- webview-ui/src/i18n/locales/es/chat.json | 6 ++-- webview-ui/src/i18n/locales/fr/chat.json | 6 ++-- webview-ui/src/i18n/locales/hi/chat.json | 6 ++-- webview-ui/src/i18n/locales/id/chat.json | 6 ++-- webview-ui/src/i18n/locales/it/chat.json | 6 ++-- webview-ui/src/i18n/locales/ja/chat.json | 6 ++-- webview-ui/src/i18n/locales/ko/chat.json | 6 ++-- webview-ui/src/i18n/locales/nl/chat.json | 6 ++-- webview-ui/src/i18n/locales/pl/chat.json | 6 ++-- webview-ui/src/i18n/locales/pt-BR/chat.json | 6 ++-- webview-ui/src/i18n/locales/ru/chat.json | 6 ++-- webview-ui/src/i18n/locales/tr/chat.json | 6 ++-- webview-ui/src/i18n/locales/vi/chat.json | 6 ++-- webview-ui/src/i18n/locales/zh-CN/chat.json | 6 ++-- webview-ui/src/i18n/locales/zh-TW/chat.json | 6 ++-- 39 files changed, 234 insertions(+), 218 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a96b42574..b27e8d097e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Zoo Code Changelog +## [3.74.0] + +### Minor Changes + +- Add Fast priority mode for OpenAI Codex, persisting the selection and sending it with requests (PR #1063 by @WebMad) +- Add higher reasoning-effort options for OpenAI-compatible models (#882 by @MINLEGO, PR #1051 by @ivanarifin) +- Fetch router-provider model metadata before making context-management decisions (PR #1053 by @JamesRobert20) +- Make Ollama model refreshes reliable, surface refresh errors, and use the base URL currently being edited (#877 by @navedmerchant, PR #878 by @navedmerchant) +- Fix Amazon Bedrock DNS resolution when using a corporate proxy (#905 by @LouisClt, PR #906 by @LouisClt) +- Add reasoning-parameter support to the Friendli provider (PR #886 by @Lee-Si-Yoon) +- Keep Save-managed settings in the local editing buffer until the user explicitly saves (#862 by @JunyongParkDev, PR #872 by @JunyongParkDev) +- Stop prompting about command output after short foreground commands have already completed (#1042 by @edelauna, PR #1043 by @app/zoomote) +- Keep Architect mode plans workspace-relative so creating `./plans` does not fail on read-only filesystem roots (#965 by @juneleung, PR #968 by @app/zoomote) +- Replace remaining user-facing Roo branding with Zoo branding (#551 by @proyectoauraorg, PR #971 by @rrewll) +- Introduce a semaphore-based task scheduler for controlled task execution (#358 by @edelauna, PR #1031 by @edelauna) +- Introduce `TaskRegistry` and migrate task-stack management to it (#367 by @edelauna, PR #1014 by @edelauna) +- Complete the API provider migration to canonical provider identifiers (#955 by @WebMad, PR #1012 by @WebMad) +- Use canonical provider identifiers for default model definitions (#954 by @WebMad, PR #991 by @WebMad) +- Use canonical provider identifiers for API model caches (#957 by @WebMad, PR #1020 by @WebMad) +- Use canonical provider identifiers in shared profile settings (#956 by @WebMad, PR #1019 by @WebMad) +- Use canonical provider identifiers throughout extension-core flows (#958 by @WebMad, PR #1022 by @WebMad) +- Use canonical provider identifiers throughout the webview (#959 by @WebMad, PR #1023 by @WebMad) +- Finish the repository-wide canonical provider identifier audit (#960 by @WebMad, PR #1030 by @WebMad) +- Centralize API service-tier types and helpers (PR #1040 by @WebMad) +- Support the platform-package layout introduced by `@vscode/ripgrep` 1.18 and later (#1024 by @saravanaraj0078-lab, PR #1032 by @edelauna) +- Update `shell-quote` to 1.9.0 to incorporate security fixes (PR #986 by @app/renovate) +- Update the development and build toolchain to Node.js 22.23.1 LTS (PR #743 by @app/renovate) +- Add a Playwright visual-regression harness for webview components (#515 by @edelauna, PR #526 by @edelauna) +- Enforce the no-floating-promises lint ratchet in `core/webview` (#949 by @edelauna, PR #950 by @morgan-coded) +- Harden the end-to-end workflow against transient VS Code binary download failures (#1044 by @edelauna, PR #1045 by @app/zoomote) +- Isolate mocked subtask fixtures to prevent flaky parent-resume tests (#1001 by @app/zoomote, PR #1002 by @app/zoomote) +- Prevent cancelled delayed mock streams from leaking between subtask end-to-end tests (PR #1074 by @app/zoomote) +- Merge the v3.72.0 release preparation branch into `main` (PR #1013 by @navedmerchant) + ## [3.72.0] ### Minor Changes diff --git a/README.md b/README.md index c3ff388c97..66b62bb7d1 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,14 @@ You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX) for this exact support, so if you are having problems or if you have question, jump on and ask. -## What's New in v3.72.0 +## What's New in v3.74.0 -- **Moonshot and Kimi Code providers** — discover Moonshot models dynamically or sign in to Kimi Code through its OAuth device flow. -- **Latest model support** — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3. -- **Improved subtask workflows** — abandon interrupted subtasks cleanly, with safer task-history invalidation and better resume coverage. -- **Expanded codebase indexing** — index Dart and plain-text files. -- **Provider reliability** — fixes for GPT-5.6 Luna with ChatGPT OAuth, LiteLLM reasoning content, and DeepSeek V4 image input. -- **Smoother setup and settings** — improved Zoo Gateway sign-in and mode-switch behavior. -- API, provider-type, dependency, localization, and CI improvements. +- **More OpenAI controls** — use Fast priority mode with OpenAI Codex and choose higher reasoning effort for OpenAI-compatible models. +- **More reliable providers and models** — improved router metadata handling, Ollama model refresh, Bedrock proxy support, and Friendli reasoning controls. +- **Smoother settings and developer workflows** — settings now preserve unsaved edits, short terminal commands complete cleanly, architect plans use workspace-relative paths, and remaining user-facing Roo branding is updated to Zoo. +- **Stronger task foundations** — new task registry and semaphore-based scheduler primitives prepare Zoo Code for safer task coordination. +- **Consistent provider architecture** — provider identifiers and service-tier primitives are now centralized across the API, core, shared types, and webview. +- Security, dependency, lint, visual-regression, and end-to-end test improvements.
🌐 Available languages @@ -66,7 +65,7 @@ for this exact support, so if you are having problems or if you have question, j - [Tiếng Việt](locales/vi/README.md) - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) -
+
--- diff --git a/locales/ca/README.md b/locales/ca/README.md index 9853926e20..78e4747f7f 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -36,15 +36,14 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o algun dubte, entra i pregunta. -## Novetats a la v3.72.0 - -- **Proveïdors Moonshot i Kimi Code** — descobreix dinàmicament els models de Moonshot o inicia la sessió a Kimi Code mitjançant el flux de dispositiu OAuth. -- **Compatibilitat amb els models més recents** — utilitza Claude Opus 5 amb tots els proveïdors, a més de Kimi K3, Gemini 3.6 Flash i MiniMax-M3. -- **Fluxos de subtasques millorats** — abandona subtasques interrompudes de manera neta, amb una invalidació més segura de l'historial de tasques i una millor cobertura de represa. -- **Indexació de la base de codi ampliada** — indexa fitxers Dart i de text pla. -- **Fiabilitat dels proveïdors** — correccions per a GPT-5.6 Luna amb ChatGPT OAuth, el contingut de raonament de LiteLLM i l'entrada d'imatges de DeepSeek V4. -- **Configuració més fluida** — millores en l'inici de sessió de Zoo Gateway i en el canvi de mode. -- Millores de l'API, els tipus de proveïdor, les dependències, la localització i la CI. +## Novetats a la v3.74.0 + +- **Més controls d'OpenAI** — utilitza el mode de prioritat Fast amb OpenAI Codex i tria un nivell de raonament més alt per als models compatibles amb OpenAI. +- **Proveïdors i models més fiables** — millores en la gestió de metadades del router, l'actualització de models d'Ollama, el suport de proxy de Bedrock i els controls de raonament de Friendli. +- **Configuració i fluxos de desenvolupament més fluids** — la configuració conserva els canvis sense desar, les ordres curtes de terminal acaben correctament, els plans d'arquitectura utilitzen rutes relatives a l'espai de treball i la marca Roo visible restant s'ha actualitzat a Zoo. +- **Fonaments de tasques més sòlids** — el nou registre de tasques i el planificador basat en semàfors preparen Zoo Code per coordinar tasques amb més seguretat. +- **Arquitectura de proveïdors coherent** — els identificadors de proveïdor i els components de nivell de servei ara estan centralitzats a l'API, el nucli, els tipus compartits i la interfície web. +- Millores de seguretat, dependències, lint, regressió visual i proves d'extrem a extrem. ## Què pot fer Zoo Code per TU? diff --git a/locales/de/README.md b/locales/de/README.md index 0706f8d992..0bf708593b 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -36,15 +36,14 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Roo→Zoo-Migrationsleitfaden](https://docs.zoocode.dev/roo-to-zoo-migration). Wir wollen Nutzer beim Umstieg so gut wie möglich unterstützen, und genau dafür sind unser [Reddit](https://www.reddit.com/r/ZooCode) und [Discord](https://discord.gg/VxfP4Vx3gX) da. Wenn du Probleme hast oder Fragen auftauchen, komm vorbei und frag nach. -## Neu in v3.72.0 - -- **Moonshot- und Kimi-Code-Anbieter** — erkenne Moonshot-Modelle dynamisch oder melde dich über den OAuth-Gerätefluss bei Kimi Code an. -- **Unterstützung für die neuesten Modelle** — nutze Claude Opus 5 anbieterübergreifend sowie Kimi K3, Gemini 3.6 Flash und MiniMax-M3. -- **Verbesserte Unteraufgaben-Workflows** — brich unterbrochene Unteraufgaben sauber ab, mit sichererer Invalidierung des Aufgabenverlaufs und besserer Abdeckung beim Fortsetzen. -- **Erweiterte Codebasis-Indizierung** — indiziere Dart- und Klartextdateien. -- **Zuverlässigere Anbieter** — Korrekturen für GPT-5.6 Luna mit ChatGPT OAuth, LiteLLM-Reasoning-Inhalte und Bildeingaben für DeepSeek V4. -- **Reibungslosere Einrichtung und Einstellungen** — verbessertes Zoo-Gateway-Login und Moduswechselverhalten. -- Verbesserungen an API, Anbietertypen, Abhängigkeiten, Lokalisierung und CI. +## Neu in v3.74.0 + +- **Mehr OpenAI-Steuerungsmöglichkeiten** — nutze den Fast-Prioritätsmodus mit OpenAI Codex und wähle für OpenAI-kompatible Modelle einen höheren Reasoning-Aufwand. +- **Zuverlässigere Anbieter und Modelle** — verbesserte Verarbeitung von Router-Metadaten, Ollama-Modellaktualisierung, Bedrock-Proxy-Unterstützung und Friendli-Reasoning-Steuerung. +- **Reibungslosere Einstellungen und Entwicklungsabläufe** — Einstellungen behalten ungespeicherte Änderungen, kurze Terminalbefehle werden sauber abgeschlossen, Architekturpläne nutzen arbeitsbereichsrelative Pfade und die verbleibende sichtbare Roo-Marke wurde auf Zoo aktualisiert. +- **Stärkere Aufgabengrundlagen** — eine neue Aufgabenregistrierung und ein semaphorbasierter Scheduler bereiten Zoo Code auf eine sicherere Aufgabenkoordination vor. +- **Einheitliche Anbieterarchitektur** — Anbieterkennungen und Service-Tier-Komponenten sind jetzt in API, Kern, gemeinsamen Typen und Webview zentralisiert. +- Verbesserungen an Sicherheit, Abhängigkeiten, Linting, visuellen Regressionstests und End-to-End-Tests. ## Was kann Zoo Code für DICH tun? diff --git a/locales/es/README.md b/locales/es/README.md index e8e67aec72..b12ff4a67a 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -36,15 +36,14 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guía de migración Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ayudar a los usuarios durante la transición, y para eso tenemos nuestro [Reddit](https://www.reddit.com/r/ZooCode) y [Discord](https://discord.gg/VxfP4Vx3gX). Si tienes problemas o alguna pregunta, entra y pregúntanos. -## Novedades de la v3.72.0 - -- **Proveedores Moonshot y Kimi Code** — descubre dinámicamente los modelos de Moonshot o inicia sesión en Kimi Code mediante su flujo de dispositivo OAuth. -- **Compatibilidad con los modelos más recientes** — usa Claude Opus 5 con todos los proveedores, además de Kimi K3, Gemini 3.6 Flash y MiniMax-M3. -- **Flujos de subtareas mejorados** — abandona subtareas interrumpidas de forma limpia, con una invalidación más segura del historial de tareas y una mejor cobertura de reanudación. -- **Indexación ampliada de la base de código** — indexa archivos Dart y de texto sin formato. -- **Fiabilidad de los proveedores** — correcciones para GPT-5.6 Luna con ChatGPT OAuth, el contenido de razonamiento de LiteLLM y la entrada de imágenes de DeepSeek V4. -- **Configuración y ajustes más fluidos** — mejoras en el inicio de sesión de Zoo Gateway y en el cambio de modo. -- Mejoras en la API, los tipos de proveedor, las dependencias, la localización y la CI. +## Novedades de la v3.74.0 + +- **Más controles de OpenAI** — usa el modo de prioridad Fast con OpenAI Codex y elige un mayor nivel de razonamiento para modelos compatibles con OpenAI. +- **Proveedores y modelos más fiables** — mejoras en el manejo de metadatos del router, la actualización de modelos de Ollama, el soporte de proxy de Bedrock y los controles de razonamiento de Friendli. +- **Ajustes y flujos de desarrollo más fluidos** — los ajustes conservan los cambios sin guardar, los comandos cortos de terminal terminan correctamente, los planes de arquitectura usan rutas relativas al espacio de trabajo y la marca Roo visible restante se ha actualizado a Zoo. +- **Bases de tareas más sólidas** — el nuevo registro de tareas y el planificador basado en semáforos preparan Zoo Code para coordinar tareas con mayor seguridad. +- **Arquitectura de proveedores coherente** — los identificadores de proveedor y los componentes de nivel de servicio ahora están centralizados en la API, el núcleo, los tipos compartidos y la webview. +- Mejoras de seguridad, dependencias, lint, regresión visual y pruebas de extremo a extremo. ## ¿Qué puede hacer Zoo Code por TI? diff --git a/locales/fr/README.md b/locales/fr/README.md index 49ad6618f5..b6a1996f57 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -36,15 +36,14 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [guide de migration Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). On veut aider au maximum pendant la transition, et notre [Reddit](https://www.reddit.com/r/ZooCode) et notre [Discord](https://discord.gg/VxfP4Vx3gX) sont là pour ça. Si tu rencontres un problème ou si tu as une question, viens demander. -## Nouveautés de la v3.72.0 - -- **Providers Moonshot et Kimi Code** — découvre dynamiquement les modèles Moonshot ou connecte-toi à Kimi Code via son flux d'appareil OAuth. -- **Prise en charge des derniers modèles** — utilise Claude Opus 5 avec tous les providers, ainsi que Kimi K3, Gemini 3.6 Flash et MiniMax-M3. -- **Workflows de sous-tâches améliorés** — abandonne proprement les sous-tâches interrompues, avec une invalidation plus sûre de l'historique et une meilleure couverture de reprise. -- **Indexation étendue de la base de code** — indexe les fichiers Dart et texte brut. -- **Fiabilité des providers** — correctifs pour GPT-5.6 Luna avec ChatGPT OAuth, le contenu de raisonnement LiteLLM et les images DeepSeek V4. -- **Configuration et réglages plus fluides** — connexion à Zoo Gateway et changement de mode améliorés. -- Améliorations de l'API, des types de providers, des dépendances, de la localisation et de la CI. +## Nouveautés de la v3.74.0 + +- **Plus de contrôles OpenAI** — utilise le mode de priorité Fast avec OpenAI Codex et choisis un effort de raisonnement supérieur pour les modèles compatibles OpenAI. +- **Providers et modèles plus fiables** — meilleure gestion des métadonnées du router, actualisation des modèles Ollama, prise en charge des proxys Bedrock et contrôles de raisonnement Friendli. +- **Paramètres et workflows de développement plus fluides** — les paramètres conservent les modifications non enregistrées, les commandes courtes du terminal se terminent correctement, les plans d'architecture utilisent des chemins relatifs à l'espace de travail et les dernières mentions visibles de Roo sont remplacées par Zoo. +- **Fondations de tâches renforcées** — le nouveau registre de tâches et le scheduler basé sur des sémaphores préparent Zoo Code à une coordination plus sûre des tâches. +- **Architecture de providers cohérente** — les identifiants de provider et les composants de niveau de service sont désormais centralisés dans l'API, le cœur, les types partagés et la webview. +- Améliorations de la sécurité, des dépendances, du lint, de la régression visuelle et des tests de bout en bout. ## Que peut faire Zoo Code pour VOUS ? diff --git a/locales/hi/README.md b/locales/hi/README.md index 10a442daf5..f1b40a702e 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -35,15 +35,14 @@ Roo Code से Zoo Code में आने के लिए एक quick guide तुम्हें [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration) में मिल जाएगी। We plan to help users as much as possible during the transition, और उसी support के लिए हमारा [Reddit](https://www.reddit.com/r/ZooCode) और [Discord](https://discord.gg/VxfP4Vx3gX) है। अगर तुम्हें कोई problem हो या कोई question हो, आकर पूछो। -## v3.72.0 में नया क्या है - -- **Moonshot और Kimi Code providers** — Moonshot models को dynamically खोजें या OAuth device flow से Kimi Code में sign in करें। -- **नवीनतम model support** — providers पर Claude Opus 5 के साथ Kimi K3, Gemini 3.6 Flash और MiniMax-M3 का उपयोग करें। -- **बेहतर subtask workflows** — interrupted subtasks को साफ़ तौर पर छोड़ें, अधिक सुरक्षित task-history invalidation और बेहतर resume coverage के साथ। -- **विस्तारित codebase indexing** — Dart और plain-text files को index करें। -- **Provider reliability** — ChatGPT OAuth के साथ GPT-5.6 Luna, LiteLLM reasoning content और DeepSeek V4 image input के लिए fixes। -- **अधिक सहज setup और settings** — Zoo Gateway sign-in और mode-switch behavior में सुधार। -- API, provider types, dependencies, localization और CI में सुधार। +## v3.74.0 में नया क्या है + +- **अधिक OpenAI controls** — OpenAI Codex के साथ Fast priority mode का उपयोग करें और OpenAI-compatible models के लिए अधिक reasoning effort चुनें। +- **अधिक भरोसेमंद providers और models** — router metadata handling, Ollama model refresh, Bedrock proxy support और Friendli reasoning controls में सुधार। +- **अधिक सहज settings और developer workflows** — settings अब unsaved edits सुरक्षित रखती हैं, छोटे terminal commands ठीक से पूरे होते हैं, architect plans workspace-relative paths का उपयोग करते हैं और बची हुई user-facing Roo branding को Zoo में अपडेट किया गया है। +- **मज़बूत task foundations** — नया task registry और semaphore-based scheduler Zoo Code को अधिक सुरक्षित task coordination के लिए तैयार करते हैं। +- **सुसंगत provider architecture** — provider identifiers और service-tier components अब API, core, shared types और webview में केंद्रीकृत हैं। +- Security, dependencies, lint, visual regression और end-to-end tests में सुधार। ## Zoo Code आपके लिए क्या कर सकता है? diff --git a/locales/id/README.md b/locales/id/README.md index c2b0139290..f273553751 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -35,15 +35,14 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di [panduan migrasi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Kami ingin membantu pengguna semaksimal mungkin selama masa transisi, dan itulah gunanya [Reddit](https://www.reddit.com/r/ZooCode) dan [Discord](https://discord.gg/VxfP4Vx3gX) kami. Kalau kamu mengalami masalah atau punya pertanyaan, langsung mampir dan tanya. -## Yang Baru di v3.72.0 - -- **Provider Moonshot dan Kimi Code** — temukan model Moonshot secara dinamis atau masuk ke Kimi Code melalui alur perangkat OAuth. -- **Dukungan model terbaru** — gunakan Claude Opus 5 di berbagai provider, ditambah Kimi K3, Gemini 3.6 Flash, dan MiniMax-M3. -- **Workflow subtask yang lebih baik** — tinggalkan subtask yang terinterupsi dengan bersih, dengan invalidasi riwayat tugas yang lebih aman dan cakupan resume yang lebih baik. -- **Indexing codebase yang lebih luas** — indeks file Dart dan teks biasa. -- **Keandalan provider** — perbaikan untuk GPT-5.6 Luna dengan ChatGPT OAuth, reasoning content LiteLLM, dan input gambar DeepSeek V4. -- **Setup dan pengaturan lebih mulus** — peningkatan login Zoo Gateway dan perilaku pergantian mode. -- Peningkatan API, tipe provider, dependensi, lokalisasi, dan CI. +## Yang Baru di v3.74.0 + +- **Kontrol OpenAI yang lebih lengkap** — gunakan mode prioritas Fast dengan OpenAI Codex dan pilih tingkat reasoning yang lebih tinggi untuk model yang kompatibel dengan OpenAI. +- **Provider dan model yang lebih andal** — peningkatan penanganan metadata router, refresh model Ollama, dukungan proxy Bedrock, dan kontrol reasoning Friendli. +- **Settings dan workflow developer yang lebih mulus** — settings kini mempertahankan edit yang belum disimpan, perintah terminal singkat selesai dengan benar, rencana architect memakai path relatif workspace, dan branding Roo yang masih terlihat telah diperbarui menjadi Zoo. +- **Fondasi task yang lebih kuat** — task registry baru dan scheduler berbasis semaphore menyiapkan Zoo Code untuk koordinasi task yang lebih aman. +- **Arsitektur provider yang konsisten** — identifier provider dan komponen service tier kini dipusatkan di API, core, shared types, dan webview. +- Peningkatan keamanan, dependensi, lint, regresi visual, dan pengujian end-to-end. ## Apa yang Bisa Zoo Code Lakukan Untuk ANDA? diff --git a/locales/it/README.md b/locales/it/README.md index 1dd8287b49..4a6abc60ad 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -36,15 +36,14 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida alla migrazione Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Vogliamo aiutare gli utenti il più possibile durante la transizione, e per questo abbiamo il nostro [Reddit](https://www.reddit.com/r/ZooCode) e il nostro [Discord](https://discord.gg/VxfP4Vx3gX). Se hai problemi o domande, passa pure e chiedi. -## Novità in v3.72.0 - -- **Provider Moonshot e Kimi Code** — rileva dinamicamente i modelli Moonshot oppure accedi a Kimi Code tramite il flusso dispositivo OAuth. -- **Supporto per i modelli più recenti** — usa Claude Opus 5 con tutti i provider, oltre a Kimi K3, Gemini 3.6 Flash e MiniMax-M3. -- **Workflow delle sottoattività migliorati** — abbandona in modo pulito le sottoattività interrotte, con un'invalidazione più sicura della cronologia e una migliore copertura della ripresa. -- **Indicizzazione della codebase ampliata** — indicizza i file Dart e di testo semplice. -- **Affidabilità dei provider** — correzioni per GPT-5.6 Luna con ChatGPT OAuth, il reasoning content di LiteLLM e l'input di immagini di DeepSeek V4. -- **Configurazione e impostazioni più fluide** — accesso a Zoo Gateway e cambio modalità migliorati. -- Miglioramenti ad API, tipi di provider, dipendenze, localizzazione e CI. +## Novità in v3.74.0 + +- **Più controlli OpenAI** — usa la modalità di priorità Fast con OpenAI Codex e scegli un livello di reasoning più elevato per i modelli compatibili con OpenAI. +- **Provider e modelli più affidabili** — gestione migliorata dei metadati del router, aggiornamento dei modelli Ollama, supporto proxy per Bedrock e controlli di reasoning per Friendli. +- **Impostazioni e workflow di sviluppo più fluidi** — le impostazioni conservano le modifiche non salvate, i comandi brevi del terminale terminano correttamente, i piani di architettura usano percorsi relativi al workspace e le ultime occorrenze visibili del brand Roo sono state aggiornate a Zoo. +- **Fondamenta più solide per le attività** — il nuovo registro delle attività e lo scheduler basato su semafori preparano Zoo Code a coordinare le attività in modo più sicuro. +- **Architettura dei provider coerente** — gli identificatori dei provider e i componenti service tier sono ora centralizzati in API, core, tipi condivisi e webview. +- Miglioramenti a sicurezza, dipendenze, lint, regressione visiva e test end-to-end. ## Cosa può fare Zoo Code per TE? diff --git a/locales/ja/README.md b/locales/ja/README.md index 08f4ce1b95..c40195fdda 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -35,15 +35,14 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo→Zoo 移行ガイド](https://docs.zoocode.dev/roo-to-zoo-migration) で確認できます。移行中のユーザーをできるだけ支援したいと考えていて、そのために [Reddit](https://www.reddit.com/r/ZooCode) と [Discord](https://discord.gg/VxfP4Vx3gX) を用意しています。困ったことや質問があれば、気軽に参加して聞いてください。 -## v3.72.0 の新機能 - -- **Moonshot と Kimi Code プロバイダー** — Moonshot モデルを動的に検出するか、OAuth デバイスフローで Kimi Code にサインインできます。 -- **最新モデルのサポート** — 各プロバイダーで Claude Opus 5 を利用できるほか、Kimi K3、Gemini 3.6 Flash、MiniMax-M3 にも対応しました。 -- **サブタスクワークフローの改善** — タスク履歴をより安全に無効化し、再開時のテスト範囲を強化しながら、中断されたサブタスクを安全に破棄できます。 -- **コードベースインデックスの拡張** — Dart ファイルとプレーンテキストファイルをインデックスできます。 -- **プロバイダーの信頼性向上** — ChatGPT OAuth での GPT-5.6 Luna、LiteLLM の reasoning content、DeepSeek V4 の画像入力を修正しました。 -- **よりスムーズなセットアップと設定** — Zoo Gateway のサインインとモード切り替えを改善しました。 -- API、プロバイダー型、依存関係、ローカライズ、CI を改善しました。 +## v3.74.0 の新機能 + +- **OpenAI の制御を強化** — OpenAI Codex で Fast 優先モードを使用し、OpenAI 互換モデルではより高い reasoning effort を選択できます。 +- **プロバイダーとモデルの信頼性を向上** — router メタデータ処理、Ollama モデル更新、Bedrock のプロキシ対応、Friendli の reasoning 制御を改善しました。 +- **設定と開発ワークフローを改善** — 未保存の編集を設定画面で保持し、短いターミナルコマンドを正しく完了し、Architect のプランにはワークスペース相対パスを使用します。残っていたユーザー向け Roo 表記も Zoo に更新しました。 +- **タスク基盤を強化** — 新しいタスクレジストリとセマフォベースのスケジューラーにより、Zoo Code のタスク連携をより安全にします。 +- **一貫したプロバイダーアーキテクチャ** — プロバイダー識別子と service tier コンポーネントを API、コア、共有型、webview 全体で一元化しました。 +- セキュリティ、依存関係、lint、ビジュアルリグレッション、エンドツーエンドテストを改善しました。 ## Zoo Codeがあなたのためにできること diff --git a/locales/ko/README.md b/locales/ko/README.md index 2d8441b101..d6726b5791 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -34,15 +34,14 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그레이션 가이드](https://docs.zoocode.dev/roo-to-zoo-migration)에서 확인할 수 있어. 전환하는 동안 사용자들을 최대한 돕고 싶고, 바로 그 지원을 위해 [Reddit](https://www.reddit.com/r/ZooCode)와 [Discord](https://discord.gg/VxfP4Vx3gX)를 운영하고 있어. 문제가 있거나 궁금한 점이 있으면 들어와서 편하게 물어봐. -## v3.72.0의 새로운 기능 - -- **Moonshot 및 Kimi Code 프로바이더** — Moonshot 모델을 동적으로 검색하거나 OAuth 기기 흐름으로 Kimi Code에 로그인하세요. -- **최신 모델 지원** — 여러 프로바이더에서 Claude Opus 5를 사용하고 Kimi K3, Gemini 3.6 Flash, MiniMax-M3도 이용하세요. -- **개선된 하위 작업 워크플로우** — 더 안전한 작업 기록 무효화와 향상된 재개 테스트 범위로 중단된 하위 작업을 깔끔하게 포기하세요. -- **확장된 코드베이스 인덱싱** — Dart 및 일반 텍스트 파일을 인덱싱하세요. -- **프로바이더 안정성** — ChatGPT OAuth의 GPT-5.6 Luna, LiteLLM reasoning content, DeepSeek V4 이미지 입력 문제를 수정했습니다. -- **더 원활한 설정** — Zoo Gateway 로그인과 모드 전환 동작을 개선했습니다. -- API, 프로바이더 유형, 의존성, 현지화 및 CI 개선. +## v3.74.0의 새로운 기능 + +- **더 다양한 OpenAI 제어** — OpenAI Codex에서 Fast 우선 모드를 사용하고 OpenAI 호환 모델의 reasoning effort를 더 높게 선택하세요. +- **더 안정적인 프로바이더와 모델** — router 메타데이터 처리, Ollama 모델 새로고침, Bedrock 프록시 지원, Friendli reasoning 제어가 개선되었습니다. +- **더 매끄러운 설정 및 개발 워크플로우** — 설정에서 저장하지 않은 편집을 유지하고, 짧은 터미널 명령을 올바르게 완료하며, Architect 계획에 워크스페이스 상대 경로를 사용합니다. 남아 있던 사용자용 Roo 브랜드도 Zoo로 업데이트했습니다. +- **더 강력한 작업 기반** — 새로운 작업 레지스트리와 세마포어 기반 스케줄러가 Zoo Code의 작업 조정을 더 안전하게 준비합니다. +- **일관된 프로바이더 아키텍처** — 프로바이더 식별자와 service tier 구성 요소를 API, 코어, 공유 타입, webview 전체에서 중앙화했습니다. +- 보안, 의존성, lint, 시각적 회귀 및 엔드투엔드 테스트를 개선했습니다. ## Zoo Code가 당신을 위해 무엇을 할 수 있을까요? diff --git a/locales/nl/README.md b/locales/nl/README.md index 6ce7a2febb..46170cd9b0 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -36,15 +36,14 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de [Roo→Zoo-migratiegids](https://docs.zoocode.dev/roo-to-zoo-migration). We willen gebruikers zo goed mogelijk helpen tijdens de overgang, en precies daarvoor zijn onze [Reddit](https://www.reddit.com/r/ZooCode) en [Discord](https://discord.gg/VxfP4Vx3gX) er. Als je ergens tegenaan loopt of vragen hebt, kom langs en vraag het. -## Nieuw in v3.72.0 - -- **Moonshot- en Kimi Code-providers** — ontdek Moonshot-modellen dynamisch of meld je aan bij Kimi Code via de OAuth-apparaatstroom. -- **Ondersteuning voor de nieuwste modellen** — gebruik Claude Opus 5 bij verschillende providers, plus Kimi K3, Gemini 3.6 Flash en MiniMax-M3. -- **Verbeterde subtaakworkflows** — breek onderbroken subtaken netjes af, met veiligere invalidatie van taakgeschiedenis en betere hervattingsdekking. -- **Uitgebreide codebase-indexering** — indexeer Dart- en plattetekstbestanden. -- **Betrouwbaardere providers** — fixes voor GPT-5.6 Luna met ChatGPT OAuth, LiteLLM-reasoningcontent en DeepSeek V4-beeldinvoer. -- **Soepelere installatie en instellingen** — verbeterde Zoo Gateway-aanmelding en moduswisseling. -- Verbeteringen aan API, providertypen, dependencies, lokalisatie en CI. +## Nieuw in v3.74.0 + +- **Meer OpenAI-bediening** — gebruik de Fast-prioriteitsmodus met OpenAI Codex en kies een hogere reasoning effort voor OpenAI-compatibele modellen. +- **Betrouwbaardere providers en modellen** — verbeterde verwerking van routermetadata, Ollama-modelverversing, Bedrock-proxyondersteuning en Friendli-reasoningbediening. +- **Soepelere instellingen en ontwikkelworkflows** — instellingen bewaren niet-opgeslagen wijzigingen, korte terminalopdrachten worden correct voltooid, architectuurplannen gebruiken werkruimterelatieve paden en de resterende zichtbare Roo-branding is bijgewerkt naar Zoo. +- **Sterkere taakfundamenten** — een nieuw taakregister en een op semaforen gebaseerde scheduler bereiden Zoo Code voor op veiligere taakcoördinatie. +- **Consistente providerarchitectuur** — provider-ID's en service-tiercomponenten zijn nu gecentraliseerd in de API, kern, gedeelde typen en webview. +- Verbeteringen aan beveiliging, dependencies, linting, visuele regressie en end-to-endtests. ## Wat kan Zoo Code voor JOU doen? diff --git a/locales/pl/README.md b/locales/pl/README.md index 83b898eb13..3de08478b6 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -34,15 +34,14 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku migracji Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chcemy jak najlepiej pomagać użytkownikom w czasie przejścia i właśnie do tego służą nasze [Reddit](https://www.reddit.com/r/ZooCode) oraz [Discord](https://discord.gg/VxfP4Vx3gX). Jeśli masz problem albo pytanie, wpadaj i pytaj. -## Nowości w v3.72.0 - -- **Providerzy Moonshot i Kimi Code** — dynamicznie wykrywaj modele Moonshot lub zaloguj się do Kimi Code przez przepływ urządzenia OAuth. -- **Obsługa najnowszych modeli** — korzystaj z Claude Opus 5 u różnych providerów, a także z Kimi K3, Gemini 3.6 Flash i MiniMax-M3. -- **Ulepszone workflow podzadań** — bezpiecznie porzucaj przerwane podzadania dzięki bezpieczniejszej invalidacji historii i lepszemu pokryciu wznawiania. -- **Rozszerzone indeksowanie bazy kodu** — indeksuj pliki Dart i pliki tekstowe. -- **Niezawodność providerów** — poprawki dla GPT-5.6 Luna z ChatGPT OAuth, reasoning content LiteLLM i obrazów DeepSeek V4. -- **Płynniejsza konfiguracja i ustawienia** — ulepszone logowanie do Zoo Gateway i przełączanie trybów. -- Ulepszenia API, typów providerów, zależności, lokalizacji i CI. +## Nowości w v3.74.0 + +- **Więcej opcji sterowania OpenAI** — używaj trybu priorytetu Fast z OpenAI Codex i wybieraj większy reasoning effort dla modeli zgodnych z OpenAI. +- **Bardziej niezawodni providerzy i modele** — ulepszona obsługa metadanych routera, odświeżanie modeli Ollama, obsługa proxy Bedrock i sterowanie reasoning w Friendli. +- **Płynniejsze ustawienia i workflow deweloperskie** — ustawienia zachowują niezapisane zmiany, krótkie polecenia terminala kończą się poprawnie, plany architekta używają ścieżek względnych wobec workspace, a pozostałe widoczne oznaczenia Roo zostały zmienione na Zoo. +- **Mocniejsze podstawy zadań** — nowy rejestr zadań i scheduler oparty na semaforach przygotowują Zoo Code do bezpieczniejszej koordynacji zadań. +- **Spójna architektura providerów** — identyfikatory providerów i komponenty service tier są teraz scentralizowane w API, core, typach współdzielonych i webview. +- Ulepszenia bezpieczeństwa, zależności, lint, regresji wizualnej i testów end-to-end. ## Co Zoo Code może zrobić dla CIEBIE? diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 931255ae80..30555bcda1 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -36,15 +36,14 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia de migração Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ajudar os usuários durante essa transição da melhor forma possível, e é exatamente para isso que temos nosso [Reddit](https://www.reddit.com/r/ZooCode) e nosso [Discord](https://discord.gg/VxfP4Vx3gX). Se você tiver algum problema ou dúvida, apareça por lá e pergunte. -## Novidades na v3.72.0 - -- **Providers Moonshot e Kimi Code** — descubra dinamicamente os modelos Moonshot ou entre no Kimi Code pelo fluxo de dispositivo OAuth. -- **Suporte aos modelos mais recentes** — use Claude Opus 5 em todos os providers, além de Kimi K3, Gemini 3.6 Flash e MiniMax-M3. -- **Workflows de subtarefas aprimorados** — abandone subtarefas interrompidas de forma limpa, com invalidação mais segura do histórico e melhor cobertura de retomada. -- **Indexação ampliada da base de código** — indexe arquivos Dart e de texto simples. -- **Confiabilidade dos providers** — correções para GPT-5.6 Luna com ChatGPT OAuth, reasoning content do LiteLLM e entrada de imagens do DeepSeek V4. -- **Configuração e ajustes mais fluidos** — melhorias no login do Zoo Gateway e na troca de modo. -- Melhorias em API, tipos de provider, dependências, localização e CI. +## Novidades na v3.74.0 + +- **Mais controles da OpenAI** — use o modo de prioridade Fast com o OpenAI Codex e escolha um nível maior de reasoning para modelos compatíveis com a OpenAI. +- **Providers e modelos mais confiáveis** — melhorias no tratamento de metadados do router, na atualização de modelos do Ollama, no suporte a proxy do Bedrock e nos controles de reasoning do Friendli. +- **Configurações e workflows de desenvolvimento mais fluidos** — as configurações preservam edições não salvas, comandos curtos do terminal terminam corretamente, planos de arquitetura usam caminhos relativos ao workspace e as últimas referências visíveis à marca Roo foram atualizadas para Zoo. +- **Bases de tarefas mais sólidas** — o novo registro de tarefas e o scheduler baseado em semáforos preparam o Zoo Code para uma coordenação de tarefas mais segura. +- **Arquitetura de providers consistente** — identificadores de provider e componentes de service tier agora estão centralizados na API, no core, nos tipos compartilhados e na webview. +- Melhorias em segurança, dependências, lint, regressão visual e testes end-to-end. ## O que o Zoo Code pode fazer por VOCÊ? diff --git a/locales/ru/README.md b/locales/ru/README.md index 4d1c716985..52f4de5bf3 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -36,15 +36,14 @@ Короткое руководство по переходу с Roo Code на Zoo Code можно найти в [гайде по миграции Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Мы хотим как можно лучше помочь пользователям во время перехода, и именно для этого у нас есть [Reddit](https://www.reddit.com/r/ZooCode) и [Discord](https://discord.gg/VxfP4Vx3gX). Если у тебя возникнут проблемы или вопросы, заходи и спрашивай. -## Что нового в v3.72.0 - -- **Провайдеры Moonshot и Kimi Code** — динамически находите модели Moonshot или входите в Kimi Code через OAuth-поток для устройств. -- **Поддержка новейших моделей** — используйте Claude Opus 5 у всех провайдеров, а также Kimi K3, Gemini 3.6 Flash и MiniMax-M3. -- **Улучшенные рабочие процессы подзадач** — безопасно завершайте прерванные подзадачи благодаря более надёжной инвалидации истории и расширенному тестированию возобновления. -- **Расширенная индексация кодовой базы** — индексируйте файлы Dart и обычные текстовые файлы. -- **Надёжность провайдеров** — исправления GPT-5.6 Luna с ChatGPT OAuth, reasoning content LiteLLM и ввода изображений DeepSeek V4. -- **Более удобные настройки** — улучшены доступ к Zoo Gateway и переключение режимов. -- Улучшения API, типов провайдеров, зависимостей, локализации и CI. +## Что нового в v3.74.0 + +- **Больше настроек OpenAI** — используйте режим приоритета Fast с OpenAI Codex и выбирайте повышенный reasoning effort для OpenAI-совместимых моделей. +- **Более надёжные провайдеры и модели** — улучшена обработка метаданных router, обновление моделей Ollama, поддержка прокси в Bedrock и управление reasoning в Friendli. +- **Более удобные настройки и рабочие процессы разработки** — настройки сохраняют несохранённые правки, короткие команды терминала корректно завершаются, планы архитектора используют пути относительно рабочей области, а оставшиеся видимые упоминания Roo заменены на Zoo. +- **Более прочная основа задач** — новый реестр задач и планировщик на семафорах готовят Zoo Code к более безопасной координации задач. +- **Единая архитектура провайдеров** — идентификаторы провайдеров и компоненты service tier теперь централизованы в API, ядре, общих типах и webview. +- Улучшения безопасности, зависимостей, lint, визуального регрессионного и сквозного тестирования. ## Что Zoo Code может сделать для ВАС? diff --git a/locales/tr/README.md b/locales/tr/README.md index 6b2e8a7f4e..458c5e571d 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -36,15 +36,14 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da soruların olursa gel ve sor. -## v3.72.0'daki Yenilikler - -- **Moonshot ve Kimi Code provider'ları** — Moonshot modellerini dinamik olarak keşfet veya OAuth cihaz akışıyla Kimi Code'da oturum aç. -- **En yeni model desteği** — provider'lar genelinde Claude Opus 5'in yanı sıra Kimi K3, Gemini 3.6 Flash ve MiniMax-M3'ü kullan. -- **Geliştirilmiş alt görev iş akışları** — daha güvenli görev geçmişi geçersiz kılma ve daha iyi devam ettirme kapsamıyla kesintiye uğramış alt görevleri temiz biçimde bırak. -- **Genişletilmiş kod tabanı indeksleme** — Dart ve düz metin dosyalarını indeksle. -- **Provider güvenilirliği** — ChatGPT OAuth ile GPT-5.6 Luna, LiteLLM reasoning content ve DeepSeek V4 görsel girdisi için düzeltmeler. -- **Daha akıcı kurulum ve ayarlar** — Zoo Gateway oturum açma ve mod değiştirme davranışı iyileştirildi. -- API, provider türleri, bağımlılıklar, yerelleştirme ve CI iyileştirmeleri. +## v3.74.0'daki Yenilikler + +- **Daha fazla OpenAI denetimi** — OpenAI Codex ile Fast öncelik modunu kullan ve OpenAI uyumlu modeller için daha yüksek reasoning effort seç. +- **Daha güvenilir provider'lar ve modeller** — router metadata işleme, Ollama model yenileme, Bedrock proxy desteği ve Friendli reasoning denetimleri iyileştirildi. +- **Daha akıcı ayarlar ve geliştirici iş akışları** — ayarlar kaydedilmemiş düzenlemeleri koruyor, kısa terminal komutları düzgün tamamlanıyor, architect planları workspace'e göreli yollar kullanıyor ve kullanıcıya görünen kalan Roo markaları Zoo olarak güncellendi. +- **Daha güçlü görev temelleri** — yeni görev kayıt sistemi ve semaphore tabanlı scheduler, Zoo Code'u daha güvenli görev koordinasyonuna hazırlıyor. +- **Tutarlı provider mimarisi** — provider tanımlayıcıları ve service tier bileşenleri artık API, core, paylaşılan türler ve webview genelinde merkezileştirildi. +- Güvenlik, bağımlılık, lint, görsel regresyon ve uçtan uca test iyileştirmeleri. ## Zoo Code SİZİN İçin Ne Yapabilir? diff --git a/locales/vi/README.md b/locales/vi/README.md index 10e493d789..98f599da28 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -35,15 +35,14 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo Code trong [hướng dẫn chuyển đổi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chúng tôi muốn hỗ trợ người dùng nhiều nhất có thể trong quá trình chuyển đổi, và đó chính là lý do chúng tôi có [Reddit](https://www.reddit.com/r/ZooCode) và [Discord](https://discord.gg/VxfP4Vx3gX). Nếu bạn gặp vấn đề hoặc có câu hỏi, cứ vào hỏi nhé. -## Điểm mới trong v3.72.0 - -- **Provider Moonshot và Kimi Code** — khám phá động các model Moonshot hoặc đăng nhập Kimi Code qua luồng thiết bị OAuth. -- **Hỗ trợ model mới nhất** — dùng Claude Opus 5 trên tất cả provider, cùng với Kimi K3, Gemini 3.6 Flash và MiniMax-M3. -- **Workflow tác vụ con được cải thiện** — từ bỏ gọn gàng các tác vụ con bị gián đoạn, với cơ chế vô hiệu hóa lịch sử an toàn hơn và phạm vi kiểm thử tiếp tục tốt hơn. -- **Mở rộng lập chỉ mục codebase** — lập chỉ mục file Dart và file văn bản thuần. -- **Độ tin cậy của provider** — sửa lỗi GPT-5.6 Luna với ChatGPT OAuth, reasoning content của LiteLLM và đầu vào hình ảnh DeepSeek V4. -- **Thiết lập và cài đặt mượt mà hơn** — cải thiện đăng nhập Zoo Gateway và hành vi chuyển chế độ. -- Cải tiến API, kiểu provider, phụ thuộc, bản địa hóa và CI. +## Điểm mới trong v3.74.0 + +- **Nhiều tùy chọn điều khiển OpenAI hơn** — dùng chế độ ưu tiên Fast với OpenAI Codex và chọn reasoning effort cao hơn cho các model tương thích OpenAI. +- **Provider và model đáng tin cậy hơn** — cải thiện xử lý metadata của router, làm mới model Ollama, hỗ trợ proxy Bedrock và điều khiển reasoning của Friendli. +- **Cài đặt và workflow phát triển mượt mà hơn** — cài đặt giữ lại các chỉnh sửa chưa lưu, lệnh terminal ngắn hoàn tất đúng cách, kế hoạch architect dùng đường dẫn tương đối với workspace và các nhãn Roo còn hiển thị cho người dùng đã được cập nhật thành Zoo. +- **Nền tảng tác vụ vững chắc hơn** — task registry mới và scheduler dựa trên semaphore chuẩn bị cho Zoo Code phối hợp tác vụ an toàn hơn. +- **Kiến trúc provider nhất quán** — identifier của provider và các thành phần service tier giờ được tập trung trong API, core, shared types và webview. +- Cải tiến bảo mật, phụ thuộc, lint, hồi quy hình ảnh và kiểm thử end-to-end. ## Zoo Code có thể làm gì cho BẠN? diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 4bc89bacb4..1525ff097a 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -32,15 +32,14 @@ 你可以在 [Roo→Zoo 迁移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到从 Roo Code 迁移到 Zoo Code 的快速说明。我们希望在大家迁移过程中尽可能提供帮助,这也是我们设立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社区的原因。如果你遇到问题或有任何疑问,欢迎加入后直接提问。 -## v3.72.0 新增内容 - -- **Moonshot 和 Kimi Code 提供商** — 动态发现 Moonshot 模型,或使用 OAuth 设备流程登录 Kimi Code。 -- **最新模型支持** — 跨提供商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 和 MiniMax-M3。 -- **改进的子任务工作流** — 干净地放弃中断的子任务,并通过更安全的任务历史失效机制和更完善的恢复测试提升可靠性。 -- **扩展代码库索引** — 为 Dart 和纯文本文件建立索引。 -- **提供商可靠性** — 修复 ChatGPT OAuth 下的 GPT-5.6 Luna、LiteLLM reasoning content 和 DeepSeek V4 图片输入问题。 -- **更顺畅的设置体验** — 改进 Zoo Gateway 登录和模式切换行为。 -- API、提供商类型、依赖、本地化和 CI 改进。 +## v3.74.0 新增内容 + +- **更多 OpenAI 控制选项** — 在 OpenAI Codex 中使用 Fast 优先模式,并为 OpenAI 兼容模型选择更高的 reasoning effort。 +- **更可靠的提供商和模型** — 改进 router 元数据处理、Ollama 模型刷新、Bedrock 代理支持和 Friendli reasoning 控制。 +- **更顺畅的设置和开发工作流** — 设置现在会保留未保存的编辑,短终端命令可正确完成,Architect 计划使用相对于工作区的路径,剩余面向用户的 Roo 品牌也已更新为 Zoo。 +- **更强大的任务基础** — 新的任务注册表和基于信号量的调度器为 Zoo Code 提供更安全的任务协调能力。 +- **一致的提供商架构** — 提供商标识符和 service tier 组件现已集中到 API、核心、共享类型和 webview 中。 +- 安全、依赖、lint、视觉回归和端到端测试改进。 ## Zoo Code 能为您做什么? diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index cddf23cccb..8961fa5349 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -32,15 +32,14 @@ 你可以在 [Roo→Zoo 遷移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到從 Roo Code 遷移到 Zoo Code 的快速說明。我們希望在大家轉移過程中盡可能提供協助,這也是我們設立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社群的原因。如果你遇到問題或有任何疑問,歡迎加入後直接提問。 -## v3.72.0 新功能 - -- **Moonshot 與 Kimi Code 供應商** — 動態探索 Moonshot 模型,或使用 OAuth 裝置流程登入 Kimi Code。 -- **最新模型支援** — 跨供應商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 與 MiniMax-M3。 -- **改善的子任務工作流程** — 乾淨地放棄中斷的子任務,並透過更安全的任務歷史失效機制與更完整的恢復測試提升可靠性。 -- **擴充程式碼庫索引** — 為 Dart 與純文字檔案建立索引。 -- **供應商可靠性** — 修正 ChatGPT OAuth 下的 GPT-5.6 Luna、LiteLLM reasoning content 與 DeepSeek V4 圖片輸入問題。 -- **更順暢的設定體驗** — 改善 Zoo Gateway 登入與模式切換行為。 -- API、供應商類型、相依套件、本地化與 CI 改善。 +## v3.74.0 新功能 + +- **更多 OpenAI 控制選項** — 在 OpenAI Codex 中使用 Fast 優先模式,並為 OpenAI 相容模型選擇更高的 reasoning effort。 +- **更可靠的供應商與模型** — 改善 router 中繼資料處理、Ollama 模型重新整理、Bedrock Proxy 支援與 Friendli reasoning 控制。 +- **更順暢的設定與開發工作流程** — 設定現在會保留未儲存的編輯,短終端機命令可正確完成,Architect 計畫使用相對於工作區的路徑,剩餘面向使用者的 Roo 品牌也已更新為 Zoo。 +- **更強大的任務基礎** — 新的任務登錄與以 semaphore 為基礎的排程器,讓 Zoo Code 能更安全地協調任務。 +- **一致的供應商架構** — 供應商識別碼與 service tier 元件現已集中到 API、核心、共用型別和 webview 中。 +- 安全性、相依套件、lint、視覺迴歸與端對端測試改善。 ## Zoo Code 能為您做什麼? diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2ee92edebc..912fed7837 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -215,7 +215,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jul-2026-v3.72.0-moonshot-kimi-models-workflows" // v3.72.0 Moonshot/Kimi providers, new models, subtask/indexing improvements + public readonly latestAnnouncementId = "jul-2026-v3.74.0-openai-provider-workflows" // v3.74.0 OpenAI controls, provider reliability, and smoother workflows public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager diff --git a/src/package.json b/src/package.json index b4d55845b0..f272137dc2 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "ZooCodeOrganization", - "version": "3.72.0", + "version": "3.74.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 11ac1d4028..b61063797b 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -343,9 +343,9 @@ "support": "Dona suport a Zoo Code fent-nos una estrella a GitHub.", "release": { "heading": "Novetats:", - "highlight1": "Proveïdors Moonshot i Kimi Code — connecta't als models de Moonshot amb detecció de models en temps real o inicia la sessió a Kimi Code mitjançant el flux de dispositiu OAuth.", - "highlight2": "Compatibilitat amb els models més recents — utilitza Claude Opus 5 amb tots els proveïdors, a més de Kimi K3, Gemini 3.6 Flash i MiniMax-M3.", - "highlight3": "Fluxos de treball més fiables — abandona subtasques interrompudes de manera neta i indexa fitxers Dart i de text pla a la teva base de codi." + "highlight1": "Més controls d'OpenAI — utilitza el mode de prioritat Fast amb OpenAI Codex i tria un nivell de raonament més alt per als models compatibles amb OpenAI.", + "highlight2": "Proveïdors i models més fiables — millores en la gestió de metadades del router, l'actualització de models d'Ollama, el suport de proxy de Bedrock i els controls de raonament de Friendli.", + "highlight3": "Configuració i fluxos de desenvolupament més fluids — la configuració conserva els canvis sense desar, les ordres curtes de terminal acaben correctament, els plans d'arquitectura utilitzen rutes relatives a l'espai de treball i la marca Roo visible restant s'ha actualitzat a Zoo." }, "cloudAgents": { "heading": "Novetats al núvol:", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 38415c87a5..30e584fcfe 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -343,9 +343,9 @@ "support": "Bitte unterstütze Zoo Code, indem du uns auf GitHub einen Stern gibst.", "release": { "heading": "Was ist neu:", - "highlight1": "Moonshot- und Kimi-Code-Anbieter — verbinde dich dank Live-Modellerkennung mit Moonshot-Modellen oder melde dich über den OAuth-Gerätefluss bei Kimi Code an.", - "highlight2": "Unterstützung für die neuesten Modelle — nutze Claude Opus 5 anbieterübergreifend sowie Kimi K3, Gemini 3.6 Flash und MiniMax-M3.", - "highlight3": "Zuverlässigere Workflows — brich unterbrochene Unteraufgaben sauber ab und indiziere Dart- und Klartextdateien in deiner Codebasis." + "highlight1": "Mehr OpenAI-Steuerungsmöglichkeiten — nutze den Fast-Prioritätsmodus mit OpenAI Codex und wähle für OpenAI-kompatible Modelle einen höheren Reasoning-Aufwand.", + "highlight2": "Zuverlässigere Anbieter und Modelle — verbesserte Verarbeitung von Router-Metadaten, Ollama-Modellaktualisierung, Bedrock-Proxy-Unterstützung und Friendli-Reasoning-Steuerung.", + "highlight3": "Reibungslosere Einstellungen und Entwicklungsabläufe — Einstellungen behalten ungespeicherte Änderungen, kurze Terminalbefehle werden sauber abgeschlossen, Architekturpläne nutzen arbeitsbereichsrelative Pfade und die verbleibende sichtbare Roo-Marke wurde auf Zoo aktualisiert." }, "cloudAgents": { "heading": "Neu in der Cloud:", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 8df0c2938a..0a4306e3b2 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -362,9 +362,9 @@ "support": "Please support Zoo Code by starring us on GitHub.", "release": { "heading": "What's New:", - "highlight1": "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", - "highlight2": "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", - "highlight3": "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase." + "highlight1": "More OpenAI controls — use Fast priority mode with OpenAI Codex and choose higher reasoning effort for OpenAI-compatible models.", + "highlight2": "More reliable providers and models — improved router metadata handling, Ollama model refresh, Bedrock proxy support, and Friendli reasoning controls.", + "highlight3": "Smoother settings and developer workflows — settings now preserve unsaved edits, short terminal commands complete cleanly, architect plans use workspace-relative paths, and remaining user-facing Roo branding is updated to Zoo." }, "cloudAgents": { "heading": "New in the Cloud:", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index d3beb38fe0..b6f8a70c1c 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -343,9 +343,9 @@ "support": "Apoya a Zoo Code dándonos una estrella en GitHub.", "release": { "heading": "Novedades:", - "highlight1": "Proveedores Moonshot y Kimi Code — conéctate a modelos de Moonshot con detección de modelos en tiempo real o inicia sesión en Kimi Code mediante su flujo de dispositivo OAuth.", - "highlight2": "Compatibilidad con los modelos más recientes — usa Claude Opus 5 con todos los proveedores, además de Kimi K3, Gemini 3.6 Flash y MiniMax-M3.", - "highlight3": "Flujos de trabajo más fiables — abandona subtareas interrumpidas de forma limpia e indexa archivos Dart y de texto sin formato en tu base de código." + "highlight1": "Más controles de OpenAI — usa el modo de prioridad Fast con OpenAI Codex y elige un mayor nivel de razonamiento para modelos compatibles con OpenAI.", + "highlight2": "Proveedores y modelos más fiables — mejoras en el manejo de metadatos del router, la actualización de modelos de Ollama, el soporte de proxy de Bedrock y los controles de razonamiento de Friendli.", + "highlight3": "Ajustes y flujos de desarrollo más fluidos — los ajustes conservan los cambios sin guardar, los comandos cortos de terminal terminan correctamente, los planes de arquitectura usan rutas relativas al espacio de trabajo y la marca Roo visible restante se ha actualizado a Zoo." }, "cloudAgents": { "heading": "Novedades en la Nube:", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 080e3cb044..72d0676bd7 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -343,9 +343,9 @@ "support": "Soutiens Zoo Code en nous donnant une étoile sur GitHub.", "release": { "heading": "Nouveautés :", - "highlight1": "Providers Moonshot et Kimi Code — connecte-toi aux modèles Moonshot avec la découverte en temps réel, ou à Kimi Code via son flux d'appareil OAuth.", - "highlight2": "Prise en charge des derniers modèles — utilise Claude Opus 5 avec tous les providers, ainsi que Kimi K3, Gemini 3.6 Flash et MiniMax-M3.", - "highlight3": "Workflows plus fiables — abandonne proprement les sous-tâches interrompues et indexe les fichiers Dart et texte brut de ta base de code." + "highlight1": "Plus de contrôles OpenAI — utilise le mode de priorité Fast avec OpenAI Codex et choisis un effort de raisonnement supérieur pour les modèles compatibles OpenAI.", + "highlight2": "Providers et modèles plus fiables — meilleure gestion des métadonnées du router, actualisation des modèles Ollama, prise en charge des proxys Bedrock et contrôles de raisonnement Friendli.", + "highlight3": "Paramètres et workflows de développement plus fluides — les paramètres conservent les modifications non enregistrées, les commandes courtes du terminal se terminent correctement, les plans d'architecture utilisent des chemins relatifs à l'espace de travail et les dernières mentions visibles de Roo sont remplacées par Zoo." }, "cloudAgents": { "heading": "Nouveautés dans le Cloud :", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index f2ac1cb20d..f022d0b35c 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -343,9 +343,9 @@ "support": "कृपया GitHub पर हमें स्टार देकर Zoo Code का समर्थन करें।", "release": { "heading": "नया क्या है:", - "highlight1": "Moonshot और Kimi Code providers — live model discovery के साथ Moonshot models से कनेक्ट करें या OAuth device flow से Kimi Code में sign in करें।", - "highlight2": "नवीनतम model support — providers पर Claude Opus 5 के साथ Kimi K3, Gemini 3.6 Flash और MiniMax-M3 का उपयोग करें।", - "highlight3": "अधिक भरोसेमंद workflows — interrupted subtasks को साफ़ तौर पर छोड़ें और अपनी codebase में Dart व plain-text files को index करें।" + "highlight1": "अधिक OpenAI controls — OpenAI Codex के साथ Fast priority mode का उपयोग करें और OpenAI-compatible models के लिए अधिक reasoning effort चुनें।", + "highlight2": "अधिक भरोसेमंद providers और models — router metadata handling, Ollama model refresh, Bedrock proxy support और Friendli reasoning controls में सुधार।", + "highlight3": "अधिक सहज settings और developer workflows — settings अब unsaved edits सुरक्षित रखती हैं, छोटे terminal commands ठीक से पूरे होते हैं, architect plans workspace-relative paths का उपयोग करते हैं और बची हुई user-facing Roo branding को Zoo में अपडेट किया गया है।" }, "cloudAgents": { "heading": "क्लाउड में नया:", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index e95a517221..3f719e101c 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -372,9 +372,9 @@ "support": "Dukung Zoo Code dengan memberi kami bintang di GitHub.", "release": { "heading": "Yang Baru:", - "highlight1": "Provider Moonshot dan Kimi Code — hubungkan ke model Moonshot dengan penemuan model secara langsung, atau masuk ke Kimi Code melalui alur perangkat OAuth.", - "highlight2": "Dukungan model terbaru — gunakan Claude Opus 5 di berbagai provider, ditambah Kimi K3, Gemini 3.6 Flash, dan MiniMax-M3.", - "highlight3": "Workflow lebih andal — tinggalkan subtask yang terinterupsi dengan bersih, serta indeks file Dart dan teks biasa di codebase-mu." + "highlight1": "Kontrol OpenAI yang lebih lengkap — gunakan mode prioritas Fast dengan OpenAI Codex dan pilih tingkat reasoning yang lebih tinggi untuk model yang kompatibel dengan OpenAI.", + "highlight2": "Provider dan model yang lebih andal — peningkatan penanganan metadata router, refresh model Ollama, dukungan proxy Bedrock, dan kontrol reasoning Friendli.", + "highlight3": "Settings dan workflow developer yang lebih mulus — settings kini mempertahankan edit yang belum disimpan, perintah terminal singkat selesai dengan benar, rencana architect memakai path relatif workspace, dan branding Roo yang masih terlihat telah diperbarui menjadi Zoo." }, "cloudAgents": { "heading": "Baru di Cloud:", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 7085edee5b..a172a9275c 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -343,9 +343,9 @@ "support": "Sostieni Zoo Code lasciandoci una stella su GitHub.", "release": { "heading": "Novità:", - "highlight1": "Provider Moonshot e Kimi Code — connettiti ai modelli Moonshot con il rilevamento dei modelli in tempo reale oppure accedi a Kimi Code tramite il flusso dispositivo OAuth.", - "highlight2": "Supporto per i modelli più recenti — usa Claude Opus 5 con tutti i provider, oltre a Kimi K3, Gemini 3.6 Flash e MiniMax-M3.", - "highlight3": "Workflow più affidabili — abbandona in modo pulito le sottoattività interrotte e indicizza i file Dart e di testo semplice nella tua codebase." + "highlight1": "Più controlli OpenAI — usa la modalità di priorità Fast con OpenAI Codex e scegli un livello di reasoning più elevato per i modelli compatibili con OpenAI.", + "highlight2": "Provider e modelli più affidabili — gestione migliorata dei metadati del router, aggiornamento dei modelli Ollama, supporto proxy per Bedrock e controlli di reasoning per Friendli.", + "highlight3": "Impostazioni e workflow di sviluppo più fluidi — le impostazioni conservano le modifiche non salvate, i comandi brevi del terminale terminano correttamente, i piani di architettura usano percorsi relativi al workspace e le ultime occorrenze visibili del brand Roo sono state aggiornate a Zoo." }, "cloudAgents": { "heading": "Novità nel Cloud:", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 8abbe5792c..a51f6d9fa0 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -343,9 +343,9 @@ "support": "GitHubでスターを付けて Zoo Code を応援してください。", "release": { "heading": "新機能:", - "highlight1": "Moonshot と Kimi Code プロバイダー — ライブモデル検出で Moonshot モデルに接続するか、OAuth デバイスフローで Kimi Code にサインインできます。", - "highlight2": "最新モデルのサポート — 各プロバイダーで Claude Opus 5 を利用できるほか、Kimi K3、Gemini 3.6 Flash、MiniMax-M3 にも対応しました。", - "highlight3": "より信頼性の高いワークフロー — 中断されたサブタスクを安全に破棄し、コードベース内の Dart ファイルとプレーンテキストファイルをインデックスできます。" + "highlight1": "OpenAI の制御を強化 — OpenAI Codex で Fast 優先モードを使用し、OpenAI 互換モデルではより高い reasoning effort を選択できます。", + "highlight2": "プロバイダーとモデルの信頼性を向上 — router メタデータ処理、Ollama モデル更新、Bedrock のプロキシ対応、Friendli の reasoning 制御を改善しました。", + "highlight3": "設定と開発ワークフローを改善 — 未保存の編集を設定画面で保持し、短いターミナルコマンドを正しく完了し、Architect のプランにはワークスペース相対パスを使用します。残っていたユーザー向け Roo 表記も Zoo に更新しました。" }, "cloudAgents": { "heading": "クラウドの新機能:", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index ac7b6c4c05..cdfecf3a10 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -343,9 +343,9 @@ "support": "GitHub에서 별표를 눌러 Zoo Code를 응원해 주세요.", "release": { "heading": "새로운 기능:", - "highlight1": "Moonshot 및 Kimi Code 프로바이더 — 실시간 모델 검색으로 Moonshot 모델에 연결하거나 OAuth 기기 흐름으로 Kimi Code에 로그인하세요.", - "highlight2": "최신 모델 지원 — 여러 프로바이더에서 Claude Opus 5를 사용하고 Kimi K3, Gemini 3.6 Flash, MiniMax-M3도 이용하세요.", - "highlight3": "더 안정적인 워크플로우 — 중단된 하위 작업을 깔끔하게 포기하고 코드베이스의 Dart 및 일반 텍스트 파일을 인덱싱하세요." + "highlight1": "더 다양한 OpenAI 제어 — OpenAI Codex에서 Fast 우선 모드를 사용하고 OpenAI 호환 모델의 reasoning effort를 더 높게 선택하세요.", + "highlight2": "더 안정적인 프로바이더와 모델 — router 메타데이터 처리, Ollama 모델 새로고침, Bedrock 프록시 지원, Friendli reasoning 제어가 개선되었습니다.", + "highlight3": "더 매끄러운 설정 및 개발 워크플로우 — 설정에서 저장하지 않은 편집을 유지하고, 짧은 터미널 명령을 올바르게 완료하며, Architect 계획에 워크스페이스 상대 경로를 사용합니다. 남아 있던 사용자용 Roo 브랜드도 Zoo로 업데이트했습니다." }, "cloudAgents": { "heading": "클라우드의 새로운 기능:", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index aa7608b1f3..06d3c483ed 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -316,9 +316,9 @@ "support": "Steun Zoo Code door ons een ster te geven op GitHub.", "release": { "heading": "Wat is nieuw:", - "highlight1": "Moonshot- en Kimi Code-providers — maak verbinding met Moonshot-modellen via live modeldetectie of meld je aan bij Kimi Code via de OAuth-apparaatstroom.", - "highlight2": "Ondersteuning voor de nieuwste modellen — gebruik Claude Opus 5 bij verschillende providers, plus Kimi K3, Gemini 3.6 Flash en MiniMax-M3.", - "highlight3": "Betrouwbaardere workflows — breek onderbroken subtaken netjes af en indexeer Dart- en plattetekstbestanden in je codebase." + "highlight1": "Meer OpenAI-bediening — gebruik de Fast-prioriteitsmodus met OpenAI Codex en kies een hogere reasoning effort voor OpenAI-compatibele modellen.", + "highlight2": "Betrouwbaardere providers en modellen — verbeterde verwerking van routermetadata, Ollama-modelverversing, Bedrock-proxyondersteuning en Friendli-reasoningbediening.", + "highlight3": "Soepelere instellingen en ontwikkelworkflows — instellingen bewaren niet-opgeslagen wijzigingen, korte terminalopdrachten worden correct voltooid, architectuurplannen gebruiken werkruimterelatieve paden en de resterende zichtbare Roo-branding is bijgewerkt naar Zoo." }, "cloudAgents": { "heading": "Nieuw in de Cloud:", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 505d8b766c..d60691ca53 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -343,9 +343,9 @@ "support": "Wesprzyj Zoo Code, dając nam gwiazdkę na GitHub.", "release": { "heading": "Co nowego:", - "highlight1": "Providerzy Moonshot i Kimi Code — łącz się z modelami Moonshot dzięki wykrywaniu modeli na żywo lub zaloguj się do Kimi Code przez przepływ urządzenia OAuth.", - "highlight2": "Obsługa najnowszych modeli — korzystaj z Claude Opus 5 u różnych providerów, a także z Kimi K3, Gemini 3.6 Flash i MiniMax-M3.", - "highlight3": "Bardziej niezawodne workflow — bezpiecznie porzucaj przerwane podzadania i indeksuj pliki Dart oraz pliki tekstowe w swojej bazie kodu." + "highlight1": "Więcej opcji sterowania OpenAI — używaj trybu priorytetu Fast z OpenAI Codex i wybieraj większy reasoning effort dla modeli zgodnych z OpenAI.", + "highlight2": "Bardziej niezawodni providerzy i modele — ulepszona obsługa metadanych routera, odświeżanie modeli Ollama, obsługa proxy Bedrock i sterowanie reasoning w Friendli.", + "highlight3": "Płynniejsze ustawienia i workflow deweloperskie — ustawienia zachowują niezapisane zmiany, krótkie polecenia terminala kończą się poprawnie, plany architekta używają ścieżek względnych wobec workspace, a pozostałe widoczne oznaczenia Roo zostały zmienione na Zoo." }, "cloudAgents": { "heading": "Nowości w chmurze:", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 45e7b227ce..cf71819ebe 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -343,9 +343,9 @@ "support": "Apoie o Zoo Code nos dando uma estrela no GitHub.", "release": { "heading": "Novidades:", - "highlight1": "Providers Moonshot e Kimi Code — conecte-se aos modelos Moonshot com descoberta de modelos em tempo real ou entre no Kimi Code pelo fluxo de dispositivo OAuth.", - "highlight2": "Suporte aos modelos mais recentes — use Claude Opus 5 em vários providers, além de Kimi K3, Gemini 3.6 Flash e MiniMax-M3.", - "highlight3": "Workflows mais confiáveis — abandone subtarefas interrompidas de forma limpa e indexe arquivos Dart e de texto simples na sua base de código." + "highlight1": "Mais controles da OpenAI — use o modo de prioridade Fast com o OpenAI Codex e escolha um nível maior de reasoning para modelos compatíveis com a OpenAI.", + "highlight2": "Providers e modelos mais confiáveis — melhorias no tratamento de metadados do router, na atualização de modelos do Ollama, no suporte a proxy do Bedrock e nos controles de reasoning do Friendli.", + "highlight3": "Configurações e workflows de desenvolvimento mais fluidos — as configurações preservam edições não salvas, comandos curtos do terminal terminam corretamente, planos de arquitetura usam caminhos relativos ao workspace e as últimas referências visíveis à marca Roo foram atualizadas para Zoo." }, "cloudAgents": { "heading": "Novidades na Nuvem:", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 5b8ae0da19..3a3c36b038 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -317,9 +317,9 @@ "support": "Поддержите Zoo Code, поставив нам звезду на GitHub.", "release": { "heading": "Что нового:", - "highlight1": "Провайдеры Moonshot и Kimi Code — подключайтесь к моделям Moonshot с динамическим обнаружением моделей или входите в Kimi Code через OAuth-поток для устройств.", - "highlight2": "Поддержка новейших моделей — используйте Claude Opus 5 у разных провайдеров, а также Kimi K3, Gemini 3.6 Flash и MiniMax-M3.", - "highlight3": "Более надёжные рабочие процессы — безопасно завершайте прерванные подзадачи и индексируйте файлы Dart и обычные текстовые файлы в кодовой базе." + "highlight1": "Больше настроек OpenAI — используйте режим приоритета Fast с OpenAI Codex и выбирайте повышенный reasoning effort для OpenAI-совместимых моделей.", + "highlight2": "Более надёжные провайдеры и модели — улучшена обработка метаданных router, обновление моделей Ollama, поддержка прокси в Bedrock и управление reasoning в Friendli.", + "highlight3": "Более удобные настройки и рабочие процессы разработки — настройки сохраняют несохранённые правки, короткие команды терминала корректно завершаются, планы архитектора используют пути относительно рабочей области, а оставшиеся видимые упоминания Roo заменены на Zoo." }, "cloudAgents": { "heading": "Новое в облаке:", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index a5c0bac756..ad4b9424b8 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -344,9 +344,9 @@ "support": "Lütfen GitHub'da bize yıldız vererek Zoo Code'u destekle.", "release": { "heading": "Yenilikler:", - "highlight1": "Moonshot ve Kimi Code provider'ları — canlı model keşfiyle Moonshot modellerine bağlan veya OAuth cihaz akışıyla Kimi Code'da oturum aç.", - "highlight2": "En yeni model desteği — provider'lar genelinde Claude Opus 5'in yanı sıra Kimi K3, Gemini 3.6 Flash ve MiniMax-M3'ü kullan.", - "highlight3": "Daha güvenilir iş akışları — kesintiye uğramış alt görevleri temiz biçimde bırak ve kod tabanındaki Dart ile düz metin dosyalarını indeksle." + "highlight1": "Daha fazla OpenAI denetimi — OpenAI Codex ile Fast öncelik modunu kullan ve OpenAI uyumlu modeller için daha yüksek reasoning effort seç.", + "highlight2": "Daha güvenilir provider'lar ve modeller — router metadata işleme, Ollama model yenileme, Bedrock proxy desteği ve Friendli reasoning denetimleri iyileştirildi.", + "highlight3": "Daha akıcı ayarlar ve geliştirici iş akışları — ayarlar kaydedilmemiş düzenlemeleri koruyor, kısa terminal komutları düzgün tamamlanıyor, architect planları workspace'e göreli yollar kullanıyor ve kullanıcıya görünen kalan Roo markaları Zoo olarak güncellendi." }, "cloudAgents": { "heading": "Cloud'daki yenilikler:", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index b5da186e43..1b21342692 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -344,9 +344,9 @@ "support": "Hãy ủng hộ Zoo Code bằng cách tặng sao cho chúng tôi trên GitHub.", "release": { "heading": "Có gì mới:", - "highlight1": "Provider Moonshot và Kimi Code — kết nối với các model Moonshot bằng tính năng khám phá model trực tiếp hoặc đăng nhập Kimi Code qua luồng thiết bị OAuth.", - "highlight2": "Hỗ trợ model mới nhất — dùng Claude Opus 5 trên nhiều provider, cùng với Kimi K3, Gemini 3.6 Flash và MiniMax-M3.", - "highlight3": "Workflow đáng tin cậy hơn — từ bỏ gọn gàng các tác vụ con bị gián đoạn và lập chỉ mục file Dart cùng file văn bản thuần trong codebase." + "highlight1": "Nhiều tùy chọn điều khiển OpenAI hơn — dùng chế độ ưu tiên Fast với OpenAI Codex và chọn reasoning effort cao hơn cho các model tương thích OpenAI.", + "highlight2": "Provider và model đáng tin cậy hơn — cải thiện xử lý metadata của router, làm mới model Ollama, hỗ trợ proxy Bedrock và điều khiển reasoning của Friendli.", + "highlight3": "Cài đặt và workflow phát triển mượt mà hơn — cài đặt giữ lại các chỉnh sửa chưa lưu, lệnh terminal ngắn hoàn tất đúng cách, kế hoạch architect dùng đường dẫn tương đối với workspace và các nhãn Roo còn hiển thị cho người dùng đã được cập nhật thành Zoo." }, "cloudAgents": { "heading": "Mới trên Cloud:", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 63b8ac69fd..d264934150 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -344,9 +344,9 @@ "support": "请在 GitHub 上为 Zoo Code 点星支持我们。", "release": { "heading": "新增功能:", - "highlight1": "Moonshot 和 Kimi Code 提供商 — 通过实时模型发现连接 Moonshot 模型,或使用 OAuth 设备流程登录 Kimi Code。", - "highlight2": "最新模型支持 — 跨提供商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 和 MiniMax-M3。", - "highlight3": "更可靠的工作流 — 干净地放弃中断的子任务,并为代码库中的 Dart 和纯文本文件建立索引。" + "highlight1": "更多 OpenAI 控制选项 — 在 OpenAI Codex 中使用 Fast 优先模式,并为 OpenAI 兼容模型选择更高的 reasoning effort。", + "highlight2": "更可靠的提供商和模型 — 改进 router 元数据处理、Ollama 模型刷新、Bedrock 代理支持和 Friendli reasoning 控制。", + "highlight3": "更顺畅的设置和开发工作流 — 设置现在会保留未保存的编辑,短终端命令可正确完成,Architect 计划使用相对于工作区的路径,剩余面向用户的 Roo 品牌也已更新为 Zoo。" }, "cloudAgents": { "heading": "云端新功能:", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index dbea150f29..722403a78b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -365,9 +365,9 @@ "support": "請在 GitHub 上為 Zoo Code 加星支持我們。", "release": { "heading": "新增功能:", - "highlight1": "Moonshot 與 Kimi Code 供應商 — 透過即時模型探索連接 Moonshot 模型,或使用 OAuth 裝置流程登入 Kimi Code。", - "highlight2": "最新模型支援 — 跨供應商使用 Claude Opus 5,以及 Kimi K3、Gemini 3.6 Flash 與 MiniMax-M3。", - "highlight3": "更可靠的工作流程 — 乾淨地放棄中斷的子任務,並為程式碼庫中的 Dart 與純文字檔案建立索引。" + "highlight1": "更多 OpenAI 控制選項 — 在 OpenAI Codex 中使用 Fast 優先模式,並為 OpenAI 相容模型選擇更高的 reasoning effort。", + "highlight2": "更可靠的供應商與模型 — 改善 router 中繼資料處理、Ollama 模型重新整理、Bedrock Proxy 支援與 Friendli reasoning 控制。", + "highlight3": "更順暢的設定與開發工作流程 — 設定現在會保留未儲存的編輯,短終端機命令可正確完成,Architect 計畫使用相對於工作區的路徑,剩餘面向使用者的 Roo 品牌也已更新為 Zoo。" }, "cloudAgents": { "heading": "雲端的新功能:", From 498a71f7990ab74202bde13d9f0a5c27af682af1 Mon Sep 17 00:00:00 2001 From: Toray Altas <6816042+taltas@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:30:16 +0000 Subject: [PATCH 85/89] docs: announce Zoo Gateway in v3.74.0 README --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 66b62bb7d1..2ae7e164e3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,20 @@ for this exact support, so if you are having problems or if you have question, j ## What's New in v3.74.0 +**Zoo Gateway is live!** + +The gateway is a single endpoint for all providers, with one balance and per-request spending/usage breakdown. + +**Setup:** + +- Add credits: https://www.zoocode.dev/dashboard/credits +- Sign in from the extension. +- In the settings, select Zoo Gateway as the provider when creating profiles for different models + +Usage and charges can be viewed in the [dashboard](https://www.zoocode.dev/dashboard). + +Models: https://www.zoocode.dev/dashboard/models + - **More OpenAI controls** — use Fast priority mode with OpenAI Codex and choose higher reasoning effort for OpenAI-compatible models. - **More reliable providers and models** — improved router metadata handling, Ollama model refresh, Bedrock proxy support, and Friendli reasoning controls. - **Smoother settings and developer workflows** — settings now preserve unsaved edits, short terminal commands complete cleanly, architect plans use workspace-relative paths, and remaining user-facing Roo branding is updated to Zoo. @@ -65,7 +79,7 @@ for this exact support, so if you are having problems or if you have question, j - [Tiếng Việt](locales/vi/README.md) - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) - + --- From b5d405eadba1c369052832d42338354f4a47ca63 Mon Sep 17 00:00:00 2001 From: Toray Altas <6816042+taltas@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:41:43 +0000 Subject: [PATCH 86/89] docs: add Zoo Gateway announcement to localized v3.74.0 READMEs --- locales/ca/README.md | 14 ++++++++++++++ locales/de/README.md | 14 ++++++++++++++ locales/es/README.md | 14 ++++++++++++++ locales/fr/README.md | 14 ++++++++++++++ locales/hi/README.md | 14 ++++++++++++++ locales/id/README.md | 14 ++++++++++++++ locales/it/README.md | 14 ++++++++++++++ locales/ja/README.md | 14 ++++++++++++++ locales/ko/README.md | 14 ++++++++++++++ locales/nl/README.md | 14 ++++++++++++++ locales/pl/README.md | 14 ++++++++++++++ locales/pt-BR/README.md | 14 ++++++++++++++ locales/ru/README.md | 14 ++++++++++++++ locales/tr/README.md | 14 ++++++++++++++ locales/vi/README.md | 14 ++++++++++++++ locales/zh-CN/README.md | 14 ++++++++++++++ locales/zh-TW/README.md | 14 ++++++++++++++ 17 files changed, 238 insertions(+) diff --git a/locales/ca/README.md b/locales/ca/README.md index 78e4747f7f..ef41b250f8 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -38,6 +38,20 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de mig ## Novetats a la v3.74.0 +**Zoo Gateway ja és aquí!** + +El gateway és un punt d'accés únic per a tots els proveïdors, amb un sol saldo i un desglossament de despeses i ús per petició. + +**Configuració:** + +- Afegeix crèdits: https://www.zoocode.dev/dashboard/credits +- Inicia sessió des de l'extensió. +- A la configuració, selecciona Zoo Gateway com a proveïdor quan creïs perfils per a diferents models + +L'ús i els càrrecs es poden consultar al [tauler](https://www.zoocode.dev/dashboard). + +Models: https://www.zoocode.dev/dashboard/models + - **Més controls d'OpenAI** — utilitza el mode de prioritat Fast amb OpenAI Codex i tria un nivell de raonament més alt per als models compatibles amb OpenAI. - **Proveïdors i models més fiables** — millores en la gestió de metadades del router, l'actualització de models d'Ollama, el suport de proxy de Bedrock i els controls de raonament de Friendli. - **Configuració i fluxos de desenvolupament més fluids** — la configuració conserva els canvis sense desar, les ordres curtes de terminal acaben correctament, els plans d'arquitectura utilitzen rutes relatives a l'espai de treball i la marca Roo visible restant s'ha actualitzat a Zoo. diff --git a/locales/de/README.md b/locales/de/README.md index 0bf708593b..6a15479f0b 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -38,6 +38,20 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Ro ## Neu in v3.74.0 +**Zoo Gateway ist live!** + +Das Gateway ist ein einziger Endpunkt für alle Anbieter – mit einem Guthaben und einer Aufschlüsselung der Ausgaben/Nutzung pro Anfrage. + +**Einrichtung:** + +- Guthaben hinzufügen: https://www.zoocode.dev/dashboard/credits +- Melde dich in der Erweiterung an. +- Wähle in den Einstellungen Zoo Gateway als Anbieter, wenn du Profile für verschiedene Modelle erstellst + +Nutzung und Kosten kannst du im [Dashboard](https://www.zoocode.dev/dashboard) einsehen. + +Modelle: https://www.zoocode.dev/dashboard/models + - **Mehr OpenAI-Steuerungsmöglichkeiten** — nutze den Fast-Prioritätsmodus mit OpenAI Codex und wähle für OpenAI-kompatible Modelle einen höheren Reasoning-Aufwand. - **Zuverlässigere Anbieter und Modelle** — verbesserte Verarbeitung von Router-Metadaten, Ollama-Modellaktualisierung, Bedrock-Proxy-Unterstützung und Friendli-Reasoning-Steuerung. - **Reibungslosere Einstellungen und Entwicklungsabläufe** — Einstellungen behalten ungespeicherte Änderungen, kurze Terminalbefehle werden sauber abgeschlossen, Architekturpläne nutzen arbeitsbereichsrelative Pfade und die verbleibende sichtbare Roo-Marke wurde auf Zoo aktualisiert. diff --git a/locales/es/README.md b/locales/es/README.md index b12ff4a67a..393a712ea2 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -38,6 +38,20 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guí ## Novedades de la v3.74.0 +**¡Zoo Gateway ya está disponible!** + +El gateway es un punto de acceso único para todos los proveedores, con un solo saldo y un desglose de gasto y uso por solicitud. + +**Configuración:** + +- Añade créditos: https://www.zoocode.dev/dashboard/credits +- Inicia sesión desde la extensión. +- En la configuración, selecciona Zoo Gateway como proveedor al crear perfiles para diferentes modelos + +El uso y los cargos se pueden ver en el [panel](https://www.zoocode.dev/dashboard). + +Modelos: https://www.zoocode.dev/dashboard/models + - **Más controles de OpenAI** — usa el modo de prioridad Fast con OpenAI Codex y elige un mayor nivel de razonamiento para modelos compatibles con OpenAI. - **Proveedores y modelos más fiables** — mejoras en el manejo de metadatos del router, la actualización de modelos de Ollama, el soporte de proxy de Bedrock y los controles de razonamiento de Friendli. - **Ajustes y flujos de desarrollo más fluidos** — los ajustes conservan los cambios sin guardar, los comandos cortos de terminal terminan correctamente, los planes de arquitectura usan rutas relativas al espacio de trabajo y la marca Roo visible restante se ha actualizado a Zoo. diff --git a/locales/fr/README.md b/locales/fr/README.md index b6a1996f57..0f3d5a38da 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -38,6 +38,20 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [gui ## Nouveautés de la v3.74.0 +**Zoo Gateway est en ligne !** + +Le gateway est un point d'accès unique pour tous les providers, avec un seul solde et un détail des dépenses et de l'utilisation par requête. + +**Configuration :** + +- Ajoute des crédits : https://www.zoocode.dev/dashboard/credits +- Connecte-toi depuis l'extension. +- Dans les paramètres, sélectionne Zoo Gateway comme provider lors de la création de profils pour différents modèles + +L'utilisation et les frais sont visibles dans le [tableau de bord](https://www.zoocode.dev/dashboard). + +Modèles : https://www.zoocode.dev/dashboard/models + - **Plus de contrôles OpenAI** — utilise le mode de priorité Fast avec OpenAI Codex et choisis un effort de raisonnement supérieur pour les modèles compatibles OpenAI. - **Providers et modèles plus fiables** — meilleure gestion des métadonnées du router, actualisation des modèles Ollama, prise en charge des proxys Bedrock et contrôles de raisonnement Friendli. - **Paramètres et workflows de développement plus fluides** — les paramètres conservent les modifications non enregistrées, les commandes courtes du terminal se terminent correctement, les plans d'architecture utilisent des chemins relatifs à l'espace de travail et les dernières mentions visibles de Roo sont remplacées par Zoo. diff --git a/locales/hi/README.md b/locales/hi/README.md index f1b40a702e..e939fdf414 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -37,6 +37,20 @@ Roo Code से Zoo Code में आने के लिए एक quick guide ## v3.74.0 में नया क्या है +**Zoo Gateway लाइव है!** + +गेटवे सभी प्रदाताओं के लिए एक ही एंडपॉइंट है, जिसमें एक बैलेंस और प्रति-अनुरोध खर्च/उपयोग का विवरण है। + +**सेटअप:** + +- क्रेडिट जोड़ें: https://www.zoocode.dev/dashboard/credits +- एक्सटेंशन से साइन इन करें। +- सेटिंग्स में, अलग-अलग मॉडलों के लिए प्रोफ़ाइल बनाते समय Zoo Gateway को प्रदाता के रूप में चुनें + +उपयोग और शुल्क [डैशबोर्ड](https://www.zoocode.dev/dashboard) में देखे जा सकते हैं। + +मॉडल: https://www.zoocode.dev/dashboard/models + - **अधिक OpenAI controls** — OpenAI Codex के साथ Fast priority mode का उपयोग करें और OpenAI-compatible models के लिए अधिक reasoning effort चुनें। - **अधिक भरोसेमंद providers और models** — router metadata handling, Ollama model refresh, Bedrock proxy support और Friendli reasoning controls में सुधार। - **अधिक सहज settings और developer workflows** — settings अब unsaved edits सुरक्षित रखती हैं, छोटे terminal commands ठीक से पूरे होते हैं, architect plans workspace-relative paths का उपयोग करते हैं और बची हुई user-facing Roo branding को Zoo में अपडेट किया गया है। diff --git a/locales/id/README.md b/locales/id/README.md index f273553751..ed3f2214ff 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -37,6 +37,20 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di ## Yang Baru di v3.74.0 +**Zoo Gateway sudah live!** + +Gateway adalah satu endpoint untuk semua penyedia, dengan satu saldo dan rincian pengeluaran/penggunaan per permintaan. + +**Penyiapan:** + +- Tambahkan kredit: https://www.zoocode.dev/dashboard/credits +- Masuk dari ekstensi. +- Di pengaturan, pilih Zoo Gateway sebagai penyedia saat membuat profil untuk berbagai model + +Penggunaan dan biaya dapat dilihat di [dasbor](https://www.zoocode.dev/dashboard). + +Model: https://www.zoocode.dev/dashboard/models + - **Kontrol OpenAI yang lebih lengkap** — gunakan mode prioritas Fast dengan OpenAI Codex dan pilih tingkat reasoning yang lebih tinggi untuk model yang kompatibel dengan OpenAI. - **Provider dan model yang lebih andal** — peningkatan penanganan metadata router, refresh model Ollama, dukungan proxy Bedrock, dan kontrol reasoning Friendli. - **Settings dan workflow developer yang lebih mulus** — settings kini mempertahankan edit yang belum disimpan, perintah terminal singkat selesai dengan benar, rencana architect memakai path relatif workspace, dan branding Roo yang masih terlihat telah diperbarui menjadi Zoo. diff --git a/locales/it/README.md b/locales/it/README.md index 4a6abc60ad..7c94773074 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -38,6 +38,20 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida al ## Novità in v3.74.0 +**Zoo Gateway è live!** + +Il gateway è un unico endpoint per tutti i provider, con un solo saldo e un dettaglio delle spese e dell'utilizzo per richiesta. + +**Configurazione:** + +- Aggiungi crediti: https://www.zoocode.dev/dashboard/credits +- Accedi dall'estensione. +- Nelle impostazioni, seleziona Zoo Gateway come provider quando crei profili per modelli diversi + +Utilizzo e addebiti sono visibili nella [dashboard](https://www.zoocode.dev/dashboard). + +Modelli: https://www.zoocode.dev/dashboard/models + - **Più controlli OpenAI** — usa la modalità di priorità Fast con OpenAI Codex e scegli un livello di reasoning più elevato per i modelli compatibili con OpenAI. - **Provider e modelli più affidabili** — gestione migliorata dei metadati del router, aggiornamento dei modelli Ollama, supporto proxy per Bedrock e controlli di reasoning per Friendli. - **Impostazioni e workflow di sviluppo più fluidi** — le impostazioni conservano le modifiche non salvate, i comandi brevi del terminale terminano correttamente, i piani di architettura usano percorsi relativi al workspace e le ultime occorrenze visibili del brand Roo sono state aggiornate a Zoo. diff --git a/locales/ja/README.md b/locales/ja/README.md index c40195fdda..fad96f67c5 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -37,6 +37,20 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo ## v3.74.0 の新機能 +**Zoo Gateway がリリースされました!** + +ゲートウェイはすべてのプロバイダーに対応する単一のエンドポイントで、1つの残高とリクエストごとの支出/使用量の内訳を提供します。 + +**セットアップ:** + +- クレジットを追加: https://www.zoocode.dev/dashboard/credits +- 拡張機能からサインインします。 +- 設定で、異なるモデルのプロファイルを作成する際にプロバイダーとして Zoo Gateway を選択します + +使用状況と料金は[ダッシュボード](https://www.zoocode.dev/dashboard)で確認できます。 + +モデル: https://www.zoocode.dev/dashboard/models + - **OpenAI の制御を強化** — OpenAI Codex で Fast 優先モードを使用し、OpenAI 互換モデルではより高い reasoning effort を選択できます。 - **プロバイダーとモデルの信頼性を向上** — router メタデータ処理、Ollama モデル更新、Bedrock のプロキシ対応、Friendli の reasoning 制御を改善しました。 - **設定と開発ワークフローを改善** — 未保存の編集を設定画面で保持し、短いターミナルコマンドを正しく完了し、Architect のプランにはワークスペース相対パスを使用します。残っていたユーザー向け Roo 表記も Zoo に更新しました。 diff --git a/locales/ko/README.md b/locales/ko/README.md index d6726b5791..84c02f82fa 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -36,6 +36,20 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그 ## v3.74.0의 새로운 기능 +**Zoo Gateway가 출시되었습니다!** + +게이트웨이는 모든 제공업체를 위한 단일 엔드포인트로, 하나의 잔액과 요청별 지출/사용량 내역을 제공합니다. + +**설정:** + +- 크레딧 추가: https://www.zoocode.dev/dashboard/credits +- 확장 프로그램에서 로그인하세요. +- 설정에서 다양한 모델의 프로필을 만들 때 Zoo Gateway를 제공업체로 선택하세요 + +사용량과 요금은 [대시보드](https://www.zoocode.dev/dashboard)에서 확인할 수 있습니다. + +모델: https://www.zoocode.dev/dashboard/models + - **더 다양한 OpenAI 제어** — OpenAI Codex에서 Fast 우선 모드를 사용하고 OpenAI 호환 모델의 reasoning effort를 더 높게 선택하세요. - **더 안정적인 프로바이더와 모델** — router 메타데이터 처리, Ollama 모델 새로고침, Bedrock 프록시 지원, Friendli reasoning 제어가 개선되었습니다. - **더 매끄러운 설정 및 개발 워크플로우** — 설정에서 저장하지 않은 편집을 유지하고, 짧은 터미널 명령을 올바르게 완료하며, Architect 계획에 워크스페이스 상대 경로를 사용합니다. 남아 있던 사용자용 Roo 브랜드도 Zoo로 업데이트했습니다. diff --git a/locales/nl/README.md b/locales/nl/README.md index 46170cd9b0..ee9fe73941 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -38,6 +38,20 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de ## Nieuw in v3.74.0 +**Zoo Gateway is live!** + +De gateway is één endpoint voor alle providers, met één saldo en een uitsplitsing van kosten en gebruik per verzoek. + +**Instellen:** + +- Tegoed toevoegen: https://www.zoocode.dev/dashboard/credits +- Log in vanuit de extensie. +- Selecteer in de instellingen Zoo Gateway als provider bij het aanmaken van profielen voor verschillende modellen + +Gebruik en kosten zijn te zien in het [dashboard](https://www.zoocode.dev/dashboard). + +Modellen: https://www.zoocode.dev/dashboard/models + - **Meer OpenAI-bediening** — gebruik de Fast-prioriteitsmodus met OpenAI Codex en kies een hogere reasoning effort voor OpenAI-compatibele modellen. - **Betrouwbaardere providers en modellen** — verbeterde verwerking van routermetadata, Ollama-modelverversing, Bedrock-proxyondersteuning en Friendli-reasoningbediening. - **Soepelere instellingen en ontwikkelworkflows** — instellingen bewaren niet-opgeslagen wijzigingen, korte terminalopdrachten worden correct voltooid, architectuurplannen gebruiken werkruimterelatieve paden en de resterende zichtbare Roo-branding is bijgewerkt naar Zoo. diff --git a/locales/pl/README.md b/locales/pl/README.md index 3de08478b6..4056f70fd0 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -36,6 +36,20 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku ## Nowości w v3.74.0 +**Zoo Gateway jest już dostępne!** + +Bramka to pojedynczy punkt końcowy dla wszystkich dostawców, z jednym saldem i rozbiciem wydatków i zużycia na żądanie. + +**Konfiguracja:** + +- Dodaj środki: https://www.zoocode.dev/dashboard/credits +- Zaloguj się z poziomu rozszerzenia. +- W ustawieniach wybierz Zoo Gateway jako dostawcę podczas tworzenia profili dla różnych modeli + +Zużycie i opłaty można sprawdzić w [panelu](https://www.zoocode.dev/dashboard). + +Modele: https://www.zoocode.dev/dashboard/models + - **Więcej opcji sterowania OpenAI** — używaj trybu priorytetu Fast z OpenAI Codex i wybieraj większy reasoning effort dla modeli zgodnych z OpenAI. - **Bardziej niezawodni providerzy i modele** — ulepszona obsługa metadanych routera, odświeżanie modeli Ollama, obsługa proxy Bedrock i sterowanie reasoning w Friendli. - **Płynniejsze ustawienia i workflow deweloperskie** — ustawienia zachowują niezapisane zmiany, krótkie polecenia terminala kończą się poprawnie, plany architekta używają ścieżek względnych wobec workspace, a pozostałe widoczne oznaczenia Roo zostały zmienione na Zoo. diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 30555bcda1..2a8720e49d 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -38,6 +38,20 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia ## Novidades na v3.74.0 +**O Zoo Gateway está no ar!** + +O gateway é um endpoint único para todos os provedores, com um único saldo e detalhamento de gastos e uso por solicitação. + +**Configuração:** + +- Adicione créditos: https://www.zoocode.dev/dashboard/credits +- Faça login pela extensão. +- Nas configurações, selecione o Zoo Gateway como provedor ao criar perfis para diferentes modelos + +O uso e as cobranças podem ser vistos no [painel](https://www.zoocode.dev/dashboard). + +Modelos: https://www.zoocode.dev/dashboard/models + - **Mais controles da OpenAI** — use o modo de prioridade Fast com o OpenAI Codex e escolha um nível maior de reasoning para modelos compatíveis com a OpenAI. - **Providers e modelos mais confiáveis** — melhorias no tratamento de metadados do router, na atualização de modelos do Ollama, no suporte a proxy do Bedrock e nos controles de reasoning do Friendli. - **Configurações e workflows de desenvolvimento mais fluidos** — as configurações preservam edições não salvas, comandos curtos do terminal terminam corretamente, planos de arquitetura usam caminhos relativos ao workspace e as últimas referências visíveis à marca Roo foram atualizadas para Zoo. diff --git a/locales/ru/README.md b/locales/ru/README.md index 52f4de5bf3..260741dbab 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -38,6 +38,20 @@ ## Что нового в v3.74.0 +**Zoo Gateway запущен!** + +Шлюз — это единая точка доступа для всех провайдеров с одним балансом и детализацией расходов и использования по каждому запросу. + +**Настройка:** + +- Пополните кредиты: https://www.zoocode.dev/dashboard/credits +- Войдите через расширение. +- В настройках выберите Zoo Gateway в качестве провайдера при создании профилей для разных моделей + +Использование и списания можно посмотреть в [панели управления](https://www.zoocode.dev/dashboard). + +Модели: https://www.zoocode.dev/dashboard/models + - **Больше настроек OpenAI** — используйте режим приоритета Fast с OpenAI Codex и выбирайте повышенный reasoning effort для OpenAI-совместимых моделей. - **Более надёжные провайдеры и модели** — улучшена обработка метаданных router, обновление моделей Ollama, поддержка прокси в Bedrock и управление reasoning в Friendli. - **Более удобные настройки и рабочие процессы разработки** — настройки сохраняют несохранённые правки, короткие команды терминала корректно завершаются, планы архитектора используют пути относительно рабочей области, а оставшиеся видимые упоминания Roo заменены на Zoo. diff --git a/locales/tr/README.md b/locales/tr/README.md index 458c5e571d..940c708227 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -38,6 +38,20 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş reh ## v3.74.0'daki Yenilikler +**Zoo Gateway yayında!** + +Gateway, tüm sağlayıcılar için tek bir uç noktadır; tek bakiye ve istek başına harcama/kullanım dökümü sunar. + +**Kurulum:** + +- Kredi ekleyin: https://www.zoocode.dev/dashboard/credits +- Uzantıdan oturum açın. +- Ayarlarda, farklı modeller için profil oluştururken sağlayıcı olarak Zoo Gateway'i seçin + +Kullanım ve ücretler [panodan](https://www.zoocode.dev/dashboard) görüntülenebilir. + +Modeller: https://www.zoocode.dev/dashboard/models + - **Daha fazla OpenAI denetimi** — OpenAI Codex ile Fast öncelik modunu kullan ve OpenAI uyumlu modeller için daha yüksek reasoning effort seç. - **Daha güvenilir provider'lar ve modeller** — router metadata işleme, Ollama model yenileme, Bedrock proxy desteği ve Friendli reasoning denetimleri iyileştirildi. - **Daha akıcı ayarlar ve geliştirici iş akışları** — ayarlar kaydedilmemiş düzenlemeleri koruyor, kısa terminal komutları düzgün tamamlanıyor, architect planları workspace'e göreli yollar kullanıyor ve kullanıcıya görünen kalan Roo markaları Zoo olarak güncellendi. diff --git a/locales/vi/README.md b/locales/vi/README.md index 98f599da28..97a1fa8061 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -37,6 +37,20 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo C ## Điểm mới trong v3.74.0 +**Zoo Gateway đã ra mắt!** + +Gateway là một điểm cuối duy nhất cho tất cả các nhà cung cấp, với một số dư và bảng phân tích chi tiêu/mức sử dụng theo từng yêu cầu. + +**Thiết lập:** + +- Thêm tín dụng: https://www.zoocode.dev/dashboard/credits +- Đăng nhập từ tiện ích mở rộng. +- Trong phần cài đặt, chọn Zoo Gateway làm nhà cung cấp khi tạo hồ sơ cho các mô hình khác nhau + +Bạn có thể xem mức sử dụng và phí trong [bảng điều khiển](https://www.zoocode.dev/dashboard). + +Mô hình: https://www.zoocode.dev/dashboard/models + - **Nhiều tùy chọn điều khiển OpenAI hơn** — dùng chế độ ưu tiên Fast với OpenAI Codex và chọn reasoning effort cao hơn cho các model tương thích OpenAI. - **Provider và model đáng tin cậy hơn** — cải thiện xử lý metadata của router, làm mới model Ollama, hỗ trợ proxy Bedrock và điều khiển reasoning của Friendli. - **Cài đặt và workflow phát triển mượt mà hơn** — cài đặt giữ lại các chỉnh sửa chưa lưu, lệnh terminal ngắn hoàn tất đúng cách, kế hoạch architect dùng đường dẫn tương đối với workspace và các nhãn Roo còn hiển thị cho người dùng đã được cập nhật thành Zoo. diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 1525ff097a..e8b7831847 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -34,6 +34,20 @@ ## v3.74.0 新增内容 +**Zoo Gateway 已上线!** + +该网关是所有提供商的统一端点,共用一个余额,并提供按请求的费用/用量明细。 + +**设置:** + +- 添加额度:https://www.zoocode.dev/dashboard/credits +- 从扩展中登录。 +- 在设置中,为不同模型创建配置文件时选择 Zoo Gateway 作为提供商 + +用量和费用可在[仪表板](https://www.zoocode.dev/dashboard)中查看。 + +模型:https://www.zoocode.dev/dashboard/models + - **更多 OpenAI 控制选项** — 在 OpenAI Codex 中使用 Fast 优先模式,并为 OpenAI 兼容模型选择更高的 reasoning effort。 - **更可靠的提供商和模型** — 改进 router 元数据处理、Ollama 模型刷新、Bedrock 代理支持和 Friendli reasoning 控制。 - **更顺畅的设置和开发工作流** — 设置现在会保留未保存的编辑,短终端命令可正确完成,Architect 计划使用相对于工作区的路径,剩余面向用户的 Roo 品牌也已更新为 Zoo。 diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 8961fa5349..c3ef601514 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -34,6 +34,20 @@ ## v3.74.0 新功能 +**Zoo Gateway 正式上線!** + +此閘道是所有供應商的單一端點,共用一個餘額,並提供逐請求的費用/用量明細。 + +**設定:** + +- 新增額度:https://www.zoocode.dev/dashboard/credits +- 從擴充功能登入。 +- 在設定中,為不同模型建立設定檔時選擇 Zoo Gateway 作為供應商 + +用量與費用可在[儀表板](https://www.zoocode.dev/dashboard)中查看。 + +模型:https://www.zoocode.dev/dashboard/models + - **更多 OpenAI 控制選項** — 在 OpenAI Codex 中使用 Fast 優先模式,並為 OpenAI 相容模型選擇更高的 reasoning effort。 - **更可靠的供應商與模型** — 改善 router 中繼資料處理、Ollama 模型重新整理、Bedrock Proxy 支援與 Friendli reasoning 控制。 - **更順暢的設定與開發工作流程** — 設定現在會保留未儲存的編輯,短終端機命令可正確完成,Architect 計畫使用相對於工作區的路徑,剩餘面向使用者的 Roo 品牌也已更新為 Zoo。 From 6c23a1c97dd5612bb88c354d9cbe093912705f59 Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 31 Jul 2026 23:28:23 +0000 Subject: [PATCH 87/89] fix: update Announcement spec to v3.74.0 and unindent README details close tag --- README.md | 3 ++- .../chat/__tests__/Announcement.spec.tsx | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2ae7e164e3..f13c2be53b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ Models: https://www.zoocode.dev/dashboard/models - [Tiếng Việt](locales/vi/README.md) - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) - + + --- diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index f17bd96a03..b99ff02ea1 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -13,7 +13,7 @@ vi.mock("@src/utils/vscode", () => ({ vi.mock("@roo/package", () => ({ Package: { - version: "3.72.0", + version: "3.74.0", }, })) @@ -41,11 +41,11 @@ vi.mock("@src/i18n/TranslationContext", () => ({ const translations: Record = { "chat:announcement.release.heading": "What's New:", "chat:announcement.release.highlight1": - "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", + "More OpenAI controls — use Fast priority mode with OpenAI Codex and choose higher reasoning effort for OpenAI-compatible models.", "chat:announcement.release.highlight2": - "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", + "More reliable providers and models — improved router metadata handling, Ollama model refresh, Bedrock proxy support, and Friendli reasoning controls.", "chat:announcement.release.highlight3": - "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase.", + "Smoother settings and developer workflows — settings now preserve unsaved edits, short terminal commands complete cleanly, architect plans use workspace-relative paths, and remaining user-facing Roo branding is updated to Zoo.", "chat:announcement.handoff.heading": "The Roo Code plugin is not going away.", } @@ -62,20 +62,20 @@ describe("Announcement", () => { it("renders the announcement title and highlights", () => { render() - expect(screen.getByText("Zoo Code 3.72.0 Released")).toBeInTheDocument() + expect(screen.getByText("Zoo Code 3.74.0 Released")).toBeInTheDocument() expect( screen.getByText( - "Moonshot and Kimi Code providers — connect to Moonshot models with live model discovery, or sign in to Kimi Code through its OAuth device flow.", + "More OpenAI controls — use Fast priority mode with OpenAI Codex and choose higher reasoning effort for OpenAI-compatible models.", ), ).toBeInTheDocument() expect( screen.getByText( - "Latest model support — use Claude Opus 5 across providers, plus Kimi K3, Gemini 3.6 Flash, and MiniMax-M3.", + "More reliable providers and models — improved router metadata handling, Ollama model refresh, Bedrock proxy support, and Friendli reasoning controls.", ), ).toBeInTheDocument() expect( screen.getByText( - "More reliable workflows — abandon interrupted subtasks cleanly, and index Dart and plain-text files in your codebase.", + "Smoother settings and developer workflows — settings now preserve unsaved edits, short terminal commands complete cleanly, architect plans use workspace-relative paths, and remaining user-facing Roo branding is updated to Zoo.", ), ).toBeInTheDocument() }) From 6c186ff8bc97816fd0cb08e84de616a69b05e77f Mon Sep 17 00:00:00 2001 From: Zeph Gillen Date: Sat, 1 Aug 2026 14:05:09 -0700 Subject: [PATCH 88/89] test(providers): assert Opus 5 effort-shape (not budget/binary) Upstream wired claude-opus-5 in budget/binary shape across the Anthropic, Vertex, OpenRouter, and Requesty spec suites. The fork adopts Opus 5 on the effort/adaptive shape (Sonnet-5 precedent), so mirror the existing opus-4-8 / sonnet-5 / fable-5 assertions: supportsReasoningEffort + no budget/binary, max_tokens at the model ceiling (128k) not the budget default, and the effort/adaptive thinking envelope. Also flips the Requesty provider getModels mock entry to the effort shape. 7 provider spec files, 241 tests pass. --- .../__tests__/anthropic-vertex.spec.ts | 8 ++++++-- src/api/providers/__tests__/anthropic.spec.ts | 15 +++++++++++---- src/api/providers/__tests__/requesty.spec.ts | 18 +++++++++++++----- .../fetchers/__tests__/openrouter.spec.ts | 6 ++++-- .../fetchers/__tests__/requesty.spec.ts | 6 ++++-- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/api/providers/__tests__/anthropic-vertex.spec.ts b/src/api/providers/__tests__/anthropic-vertex.spec.ts index 87cf5733f9..3d46aec4fd 100644 --- a/src/api/providers/__tests__/anthropic-vertex.spec.ts +++ b/src/api/providers/__tests__/anthropic-vertex.spec.ts @@ -1103,8 +1103,12 @@ describe("VertexHandler", () => { expect(model.id).toBe("claude-opus-5") expect(model.info.maxTokens).toBe(8192) expect(model.info.contextWindow).toBe(1_000_000) - expect(model.info.supportsReasoningBinary).toBe(true) - expect(model.info.supportsReasoningBudget).toBe(true) + // Fork shapes Opus 5 as effort/adaptive on Vertex (mirrors Opus 4.8 / Sonnet 5 — + // the registry shape drives the payload, Vertex has no provider-side guard), not budget/binary. + expect(model.info.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.supportsReasoningBudget).toBeUndefined() + expect(model.info.supportsReasoningBinary).toBeUndefined() expect(model.info.supportsPromptCache).toBe(true) expect(model.info.supportsTemperature).toBe(false) }) diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 068adc7067..0c95333d7d 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -527,9 +527,12 @@ describe("AnthropicHandler", () => { const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + // Opus 5 uses effort-based adaptive thinking (default "high"), not budget/binary. + // Effort-shape models ignore modelMaxTokens, so max_tokens stays at the model's ceiling (128k). expect(requestBody?.thinking).toEqual({ type: "adaptive" }) + expect((requestBody as any)?.output_config).toEqual({ effort: "high" }) expect(requestBody?.temperature).toBeUndefined() - expect(requestBody?.max_tokens).toBe(32768) + expect(requestBody?.max_tokens).toBe(128000) expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") }) @@ -755,12 +758,16 @@ describe("AnthropicHandler", () => { expect(model.id).toBe("claude-opus-5") expect(model.info.maxTokens).toBe(128000) expect(model.info.contextWindow).toBe(1000000) - expect(model.maxTokens).toBe(8192) - expect(model.info.supportsReasoningBinary).toBe(true) - expect(model.info.supportsReasoningBudget).toBe(true) + // Local fork shapes Opus 5 as effort/adaptive (Sonnet-5 precedent), not upstream's budget/binary. + expect(model.info.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.supportsReasoningBudget).toBeUndefined() + expect(model.info.supportsReasoningBinary).toBeUndefined() expect(model.info.supportsPromptCache).toBe(true) expect(model.info.supportsTemperature).toBe(false) expect(model.reasoningBudget).toBeUndefined() + expect(model.reasoningEffort).toBe("high") + expect(model.maxTokens).toBe(128000) }) it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => { diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index a823fcf4da..478f0f1bf2 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -91,8 +91,12 @@ vitest.mock("../fetchers/modelCache", () => ({ contextWindow: 1000000, supportsImages: true, supportsPromptCache: true, - supportsReasoningBudget: true, - supportsReasoningBinary: true, + // Local fork: Opus 5 uses the effort/adaptive shape (not budget/binary), mirroring + // the requesty fetcher's claude-opus-5 patch. + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], + requiredReasoningEffort: true, + reasoningEffort: "high", + supportsReasoningDisplay: true, supportsTemperature: false, inputPrice: 5, outputPrice: 25, @@ -351,11 +355,13 @@ describe("RequestyHandler", () => { const generator = handler.createMessage("test system prompt", [{ role: "user" as const, content: "test" }]) await generator.next() + // Opus 5 uses the effort/adaptive envelope (like Sonnet 5); effort-shape ignores + // modelMaxTokens, so max_tokens stays at the model ceiling (128k), not 32768. expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ model: "anthropic/claude-opus-5", - max_tokens: 32768, - thinking: { type: "adaptive" }, + max_tokens: 128000, + thinking: { thinking: { type: "adaptive" }, output_config: { effort: "high" } }, temperature: undefined, }), ) @@ -660,9 +666,11 @@ describe("RequestyHandler", () => { await handler.completePrompt("test prompt") + // Effort-shape Opus 5: max_tokens is the model ceiling (128k), not the budget-path + // default of 8192. completePrompt sends no reasoning fields. expect(mockCreate).toHaveBeenCalledWith({ model: "anthropic/claude-opus-5", - max_tokens: 8192, + max_tokens: 128000, messages: [{ role: "system", content: "test prompt" }], temperature: undefined, }) diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index 9b4d2aa939..25d590d980 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -402,8 +402,10 @@ describe("OpenRouter API", () => { expect(result.maxTokens).toBe(128000) expect(result.contextWindow).toBe(1000000) expect(result.supportsTemperature).toBe(false) - expect(result.supportsReasoningBudget).toBe(true) - expect(result.supportsReasoningBinary).toBe(true) + // Fork shapes Opus 5 as effort/adaptive (mirrors opus/fable/sonnet), not budget/binary. + expect(result.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.requiredReasoningEffort).toBe(true) + expect(result.supportsReasoningBudget).toBe(false) }) it("sets horizon-alpha model to 32k max tokens", () => { diff --git a/src/api/providers/fetchers/__tests__/requesty.spec.ts b/src/api/providers/fetchers/__tests__/requesty.spec.ts index 9a411b42a6..4b6a16c9fe 100644 --- a/src/api/providers/fetchers/__tests__/requesty.spec.ts +++ b/src/api/providers/fetchers/__tests__/requesty.spec.ts @@ -101,8 +101,10 @@ describe("getRequestyModels", () => { const opus5 = models["anthropic/claude-opus-5"] expect(opus5).toBeDefined() - expect(opus5.supportsReasoningBudget).toBe(true) - expect(opus5.supportsReasoningBinary).toBe(true) + // Fork shapes Opus 5 as effort/adaptive (mirrors the sonnet-5 override), not budget/binary. + expect(opus5.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(opus5.requiredReasoningEffort).toBe(true) + expect(opus5.supportsReasoningBudget).toBe(false) expect(opus5.supportsTemperature).toBe(false) }) From ba8a4c2a756928a08fd090df8a297ef6a26ab171 Mon Sep 17 00:00:00 2001 From: Zeph Gillen Date: Sat, 1 Aug 2026 14:11:59 -0700 Subject: [PATCH 89/89] docs(fork): record the v3.74.0 sync Add the v3.74.0 sync-log entry (merge-base 13c803b7d, 87 commits / 447 files, 6-file conflict surface) and the decisions: Opus 5 adopted on the fork effort-shape (Sonnet-5 precedent), GPT-5.6 resolved per the pre-baked Feature 2 recipe, the canonical provider-identifier migration (only providers/index.ts conflicted), the new ANTHROPIC_API_PROTOCOL / OPENAI_API_PROTOCOL exports, service-tier centralization (a non-event for the fork), and the Node 20.20.2 -> 22.23.1 toolchain bump. Mark Opus 5 + Sonnet 5 as effort-shape in the divergence table. --- FORK.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/FORK.md b/FORK.md index d6dd5674fe..710dfb257f 100644 --- a/FORK.md +++ b/FORK.md @@ -36,7 +36,7 @@ across merges. `AGENTS.md` only points here. | # | Local feature | Origin commit(s) | Nature | | --- | ---------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------- | -| 1 | Effort-based Anthropic reasoning (Opus 4.6/4.7/4.8) | `fd93c5bde`, `64fc5fc98` | modifies shared provider logic | +| 1 | Effort-based Anthropic reasoning (Opus 4.6/4.7/4.8/5, Sonnet 5) | `fd93c5bde`, `64fc5fc98`, `60f056b1c` | modifies shared provider logic | | 2 | OpenRouter effort mirroring + gpt-5.5/5.6 defs (5.6 now also upstream — collision) | `062657a7d`, `64fc5fc98`, `ded80951d` | modifies shared fetcher/registry | | 3 | Claude Fable 5 + safety-refusal handling | `811b5ca55` | modifies shared provider logic | | 4 | `"max"` reasoningEffort i18n label | `dd675fd3b` | mechanical i18n | @@ -177,6 +177,40 @@ don't investigate them as regressions. ## Sync log / decisions +- **v3.74.0** (from v3.68.0 line, merge-base `13c803b7d` — authoritative fork-point, not the shadow + tag) — 87 upstream commits / 447 files; real conflict surface was **6 code files** + release + mechanics (CHANGELOG / README / i18n `chat.json` / `src/package.json`). **Decisions:** + - **Claude Opus 5** (upstream #1010, shipped budget/binary) adopted on the fork **effort-shape** — + same call as Sonnet 5, same reason (adaptive-only, 400s on `budget_tokens`). Converted across the + Anthropic + Vertex registries and the OpenRouter + Requesty fetchers; **Bedrock unchanged** (the + shared `isAdaptiveThinkingModel` guard already matches `opus-5`). Default effort `high` (tunable; + `xhigh` is in the array) — revisit if you want `xhigh` as the flagship default. + - **GPT-5.6** collision resolved per the Feature 2 pre-baked recipe: kept the fork's Luna 1.05M + `contextWindow` + `longContextPricing` + effort defaults (sol `high`, luna `low`) + descriptions + + fork-only `gpt-5.5-pro`; took upstream's cacheWrites/tiers (already reconciled in `ae3547c1d`). + Luna 1.05M vs upstream 400K is **still unverified** against OpenAI's docs. + - **Canonical provider-identifier migration** (WebMad, ~15 PRs across 3.72/3.74): only + `packages/types/providers/index.ts` textually conflicted — re-expressed the fork's `openai-native` + default onto the new `providerIdentifiers.openaiNative` case label (dropped upstream's + `TODO(#992)` `"gpt-4o"` fallback). `modelCache.ts` / `provider-settings.ts` / `shared/api.ts` + auto-merged clean (the fork's lone `modelCache.ts` change was an incidental Prettier reformat, + byte-identical to upstream's). Canonical ids live in the new `packages/types/provider-identifiers.ts`. + - **New upstream exports the fork's registries now carry:** `ANTHROPIC_API_PROTOCOL` + (`providers/anthropic.ts`) and `OPENAI_API_PROTOCOL` (`providers/openai.ts`), both imported by + `provider-settings.ts`. When resolving those two conflicted files by taking the fork side, **re-add + the `_API_PROTOCOL` export line** or `provider-settings.ts` fails to compile. + - **Service-tier centralization** (#1040): a non-event for the fork — tiers moved to + `packages/types/model.ts` (`OpenAiServiceTier`, `serviceTierSchema`) and `applyLongContextPricing` + lives in `src/shared/cost.ts`, none of which the fork touches. No duplication to reconcile. + - **Node toolchain 20.20.2 → 22.23.1** (`engines` + `.nvmrc`, took upstream's). Local env needs + `fnm install 22.23.1` then `corepack prepare pnpm@10.8.1 --activate` — pnpm does **not** carry over + across the fnm Node switch. pnpm itself stays 10.8.1. + - Code-index Feature 5 files did **not** conflict — upstream's 3.72 code-index work (Dart + plaintext + indexing) is in `parser.ts` / `supported-extensions.ts`, disjoint from the fork's config/scope layer. + Verified on Node 22.23.1: `@roo-code/types` + `zoo-code` `check-types` clean; `pnpm build` green + (4/4 packages); the 7 affected provider spec files pass (241 tests). Baseline: reasoning/model-params + fixture-drift failures re-derive on Node 22 — check failure identity, not the old "22" count. + - **v3.68.0** (from v3.62.0-era, merge-base `8c3ae1e8b`) — 63 upstream commits / 337 files, but the real conflict surface was ~14 code files, essentially all from upstream's **Claude Sonnet 5** (#778). **Decision:** adopt Sonnet 5 on the fork's **effort-shape** (not upstream's budget/binary)