From c817271780934d2187a8ab02096e6d617249bc8a Mon Sep 17 00:00:00 2001 From: Naved Date: Fri, 26 Jun 2026 18:19:13 -0700 Subject: [PATCH 1/3] feat(semble): upgrade to v0.4.1, flatten result parsing, localize status messages - Bump SEMBLE_VERSION v0.3.1 -> v0.4.1 and refresh SEMBLE_SHA256 checksums - Adapt to semble v0.4.0+ flat JSON output (no chunk wrapper); remove SembleChunk and flatten SembleSearchResult (file_path, start_line, end_line, score, content) - Update provider.ts and semble-cli.ts parsing accordingly - Version-prefix the local archive cache path so a stale archive from a previous semble version is never reused or verified against the new checksum; remove partial archive before re-downloading - Add cleanupStaleArchives to best-effort sweep orphaned archives after a version upgrade - Localize SembleProvider status strings via i18n embeddings:semble.* keys across all 18 locales - Update provider/cli/downloader unit tests for the new shape and version-prefixed cache path Closes #733 --- src/i18n/locales/ca/embeddings.json | 8 + src/i18n/locales/de/embeddings.json | 8 + src/i18n/locales/en/embeddings.json | 8 + src/i18n/locales/es/embeddings.json | 8 + src/i18n/locales/fr/embeddings.json | 8 + src/i18n/locales/hi/embeddings.json | 8 + src/i18n/locales/id/embeddings.json | 8 + src/i18n/locales/it/embeddings.json | 8 + src/i18n/locales/ja/embeddings.json | 8 + src/i18n/locales/ko/embeddings.json | 8 + src/i18n/locales/nl/embeddings.json | 8 + src/i18n/locales/pl/embeddings.json | 8 + src/i18n/locales/pt-BR/embeddings.json | 8 + src/i18n/locales/ru/embeddings.json | 8 + src/i18n/locales/tr/embeddings.json | 8 + src/i18n/locales/vi/embeddings.json | 8 + src/i18n/locales/zh-CN/embeddings.json | 8 + src/i18n/locales/zh-TW/embeddings.json | 8 + .../semble/__tests__/provider.spec.ts | 227 +++++++----------- .../semble/__tests__/semble-cli.spec.ts | 66 ++--- .../__tests__/semble-downloader.spec.ts | 88 +++++-- src/services/code-index/semble/index.ts | 9 +- src/services/code-index/semble/provider.ts | 34 +-- src/services/code-index/semble/semble-cli.ts | 17 +- .../code-index/semble/semble-downloader.ts | 57 ++++- src/services/code-index/semble/types.ts | 26 +- 26 files changed, 424 insertions(+), 244 deletions(-) diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 9ceec7d05c..00a8284c58 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexació requereix una carpeta de workspace oberta", "indexingStopped": "Indexació aturada per l'usuari.", "indexingStoppedPartial": "Indexació aturada. Dades d'índex parcials conservades." + }, + "semble": { + "downloadingBinary": "Descarregant el binari de semble...", + "ready": "Semble està llest. Les cerques s'indexen sobre la marxa.", + "unsupportedPlatform": "Semble no és compatible amb aquesta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Error en descarregar semble: {{errorMessage}}", + "checkFailed": "Comprovació de semble fallida: {{errorMessage}}", + "providerReset": "Proveïdor de Semble restablert. La memòria cau al disc es conserva fins a la propera reconstrucció." } } diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 766d31d5ba..a52d43bbc1 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexierung erfordert einen offenen Workspace-Ordner", "indexingStopped": "Indexierung vom Benutzer gestoppt.", "indexingStoppedPartial": "Indexierung gestoppt. Teilweise Indexdaten beibehalten." + }, + "semble": { + "downloadingBinary": "Semble-Binary wird heruntergeladen...", + "ready": "Semble ist bereit. Suchen erfolgen spontan.", + "unsupportedPlatform": "Semble wird auf dieser Plattform nicht unterstützt ({{platform}}-{{arch}}).", + "downloadFailed": "Fehler beim Herunterladen von Semble: {{errorMessage}}", + "checkFailed": "Semble-Überprüfung fehlgeschlagen: {{errorMessage}}", + "providerReset": "Semble-Anbieter zurückgesetzt. Der Cache auf der Festplatte bleibt bis zum nächsten Neuaufbau erhalten." } } diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 7777af9027..8cbfdf8e8e 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexing requires an open workspace folder", "indexingStopped": "Indexing stopped by user.", "indexingStoppedPartial": "Indexing stopped. Partial index data preserved." + }, + "semble": { + "downloadingBinary": "Downloading semble binary...", + "ready": "Semble is ready. Searches index on-the-fly.", + "unsupportedPlatform": "Semble is not supported on this platform ({{platform}}-{{arch}}).", + "downloadFailed": "Failed to download semble: {{errorMessage}}", + "checkFailed": "Semble check failed: {{errorMessage}}", + "providerReset": "Semble provider reset. On-disk cache remains until next rebuild." } } diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index 930404de1f..345df2e112 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "La indexación requiere una carpeta de workspace abierta", "indexingStopped": "Indexación detenida por el usuario.", "indexingStoppedPartial": "Indexación detenida. Datos de índice parciales conservados." + }, + "semble": { + "downloadingBinary": "Descargando el binario de semble...", + "ready": "Semble está listo. Las búsquedas se indexan sobre la marcha.", + "unsupportedPlatform": "Semble no es compatible con esta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Error al descargar semble: {{errorMessage}}", + "checkFailed": "Verificación de semble fallida: {{errorMessage}}", + "providerReset": "Proveedor de Semble restablecido. La caché en disco se conserva hasta la próxima reconstrucción." } } diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index 7de086307e..ef9e56f201 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "L'indexation nécessite l'ouverture d'un dossier workspace", "indexingStopped": "Indexation arrêtée par l'utilisateur.", "indexingStoppedPartial": "Indexation arrêtée. Données d'index partielles conservées." + }, + "semble": { + "downloadingBinary": "Téléchargement du binaire semble...", + "ready": "Semble est prêt. Les recherches s'indexent à la volée.", + "unsupportedPlatform": "Semble n'est pas pris en charge sur cette plateforme ({{platform}}-{{arch}}).", + "downloadFailed": "Échec du téléchargement de semble : {{errorMessage}}", + "checkFailed": "Vérification de semble échouée : {{errorMessage}}", + "providerReset": "Fournisseur Semble réinitialisé. Le cache sur disque reste jusqu'à la prochaine reconstruction." } } diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 9c7f9ca50a..0dc82594b8 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है", "indexingStopped": "उपयोगकर्ता द्वारा इंडेक्सिंग रोकी गई।", "indexingStoppedPartial": "इंडेक्सिंग रोकी गई। आंशिक इंडेक्स डेटा संरक्षित।" + }, + "semble": { + "downloadingBinary": "semble बाइनरी डाउनलोड हो रहा है...", + "ready": "Semble तैयार है। खोजें ऑन-द-फ्लाई इंडेक्स होती हैं।", + "unsupportedPlatform": "इस प्लेटफ़ॉर्म पर Semble समर्थित नहीं है ({{platform}}-{{arch}})।", + "downloadFailed": "semble डाउनलोड करने में विफल: {{errorMessage}}", + "checkFailed": "Semble जाँच विफल: {{errorMessage}}", + "providerReset": "Semble प्रदाता रीसेट किया गया। डिस्क कैश अगले रीबिल्ड तक बना रहता है।" } } diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index 955a039eff..90f3ceadcf 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Pengindeksan memerlukan folder workspace yang terbuka", "indexingStopped": "Pengindeksan dihentikan oleh pengguna.", "indexingStoppedPartial": "Pengindeksan dihentikan. Data indeks parsial dipertahankan." + }, + "semble": { + "downloadingBinary": "Mengunduh biner semble...", + "ready": "Semble siap. Pencarian diindeks secara langsung.", + "unsupportedPlatform": "Semble tidak didukung di platform ini ({{platform}}-{{arch}}).", + "downloadFailed": "Gagal mengunduh semble: {{errorMessage}}", + "checkFailed": "Pemeriksaan semble gagal: {{errorMessage}}", + "providerReset": "Penyedia Semble direset. Cache di disk tetap ada hingga pembangunan ulang berikutnya." } } diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index b7314c244d..58f9c8ac80 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "L'indicizzazione richiede una cartella di workspace aperta", "indexingStopped": "Indicizzazione interrotta dall'utente.", "indexingStoppedPartial": "Indicizzazione interrotta. Dati di indice parziali conservati." + }, + "semble": { + "downloadingBinary": "Download del binario semble in corso...", + "ready": "Semble è pronto. Le ricerche vengono indicizzate al volo.", + "unsupportedPlatform": "Semble non è supportato su questa piattaforma ({{platform}}-{{arch}}).", + "downloadFailed": "Download di semble fallito: {{errorMessage}}", + "checkFailed": "Verifica di semble fallita: {{errorMessage}}", + "providerReset": "Provider Semble ripristinato. La cache su disco rimane fino alla prossima ricostruzione." } } diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index ce7150cf1c..9f1b27a86a 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です", "indexingStopped": "ユーザーによりインデックス作成が停止されました。", "indexingStoppedPartial": "インデックス作成が停止されました。部分的なインデックスデータは保持されています。" + }, + "semble": { + "downloadingBinary": "sembleバイナリをダウンロード中...", + "ready": "Sembleの準備ができました。検索はその場でインデックス化されます。", + "unsupportedPlatform": "このプラットフォームではSembleはサポートされていません ({{platform}}-{{arch}})。", + "downloadFailed": "sembleのダウンロードに失敗しました: {{errorMessage}}", + "checkFailed": "Sembleの確認に失敗しました: {{errorMessage}}", + "providerReset": "Sembleプロバイダーをリセットしました。次回の再構築までディスクキャッシュは残ります。" } } diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 436fa985c0..ad837703cd 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다", "indexingStopped": "사용자에 의해 인덱싱이 중지되었습니다.", "indexingStoppedPartial": "인덱싱이 중지되었습니다. 부분 인덱스 데이터가 보존되었습니다." + }, + "semble": { + "downloadingBinary": "semble 바이너리 다운로드 중...", + "ready": "Semble가 준비되었습니다. 검색은 즉시 인덱싱됩니다.", + "unsupportedPlatform": "이 플랫폼에서는 Semble가 지원되지 않습니다 ({{platform}}-{{arch}}).", + "downloadFailed": "semble 다운로드 실패: {{errorMessage}}", + "checkFailed": "Semble 확인 실패: {{errorMessage}}", + "providerReset": "Semble 공급자가 재설정되었습니다. 디스크 캐시는 다음 재구축까지 유지됩니다." } } diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index 01e68683d3..3aa562a8e9 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexering vereist een geopende workspace map", "indexingStopped": "Indexering gestopt door gebruiker.", "indexingStoppedPartial": "Indexering gestopt. Gedeeltelijke indexgegevens bewaard." + }, + "semble": { + "downloadingBinary": "Semble-binary downloaden...", + "ready": "Semble is klaar. Zoekopdrachten worden direct geïndexeerd.", + "unsupportedPlatform": "Semble wordt niet ondersteund op dit platform ({{platform}}-{{arch}}).", + "downloadFailed": "Downloaden van semble mislukt: {{errorMessage}}", + "checkFailed": "Semble-controle mislukt: {{errorMessage}}", + "providerReset": "Semble-provider gereset. Schijfcache blijft behouden tot de volgende heropbouw." } } diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 0ef846b2cc..1aba2ec5d0 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indeksowanie wymaga otwartego folderu workspace", "indexingStopped": "Indeksowanie zatrzymane przez użytkownika.", "indexingStoppedPartial": "Indeksowanie zatrzymane. Częściowe dane indeksu zachowane." + }, + "semble": { + "downloadingBinary": "Pobieranie binariów semble...", + "ready": "Semble jest gotowy. Wyszukiwania są indeksowane w locie.", + "unsupportedPlatform": "Semble nie jest obsługiwany na tej platformie ({{platform}}-{{arch}}).", + "downloadFailed": "Nie udało się pobrać semble: {{errorMessage}}", + "checkFailed": "Sprawdzenie semble nie powiodło się: {{errorMessage}}", + "providerReset": "Dostawca Semble zresetowany. Pamięć podręczna na dysku pozostaje do kolejnej przebudowy." } } diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index 9cdf775e76..ec13b35dcc 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "A indexação requer uma pasta de workspace aberta", "indexingStopped": "Indexação interrompida pelo usuário.", "indexingStoppedPartial": "Indexação interrompida. Dados de índice parciais preservados." + }, + "semble": { + "downloadingBinary": "Baixando o binário do semble...", + "ready": "Semble está pronto. As buscas são indexadas em tempo real.", + "unsupportedPlatform": "Semble não é suportado nesta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Falha ao baixar semble: {{errorMessage}}", + "checkFailed": "Verificação do semble falhou: {{errorMessage}}", + "providerReset": "Provedor Semble redefinido. O cache em disco permanece até a próxima reconstrução." } } diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index 873b1c0630..b376cef32a 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства", "indexingStopped": "Индексация остановлена пользователем.", "indexingStoppedPartial": "Индексация остановлена. Частичные данные индекса сохранены." + }, + "semble": { + "downloadingBinary": "Загрузка бинарного файла semble...", + "ready": "Semble готов. Поиск индексируется на лету.", + "unsupportedPlatform": "Semble не поддерживается на этой платформе ({{platform}}-{{arch}}).", + "downloadFailed": "Не удалось загрузить semble: {{errorMessage}}", + "checkFailed": "Проверка semble не удалась: {{errorMessage}}", + "providerReset": "Провайдер Semble сброшен. Дисковый кеш сохраняется до следующей пересборки." } } diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 30b703a93f..dc33b8f974 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "İndeksleme açık bir workspace klasörü gerektirir", "indexingStopped": "İndeksleme kullanıcı tarafından durduruldu.", "indexingStoppedPartial": "İndeksleme durduruldu. Kısmi indeks verileri korundu." + }, + "semble": { + "downloadingBinary": "semble ikili dosyası indiriliyor...", + "ready": "Semble hazır. Aramalar anında indekslenir.", + "unsupportedPlatform": "Semble bu platformda desteklenmiyor ({{platform}}-{{arch}}).", + "downloadFailed": "semble indirilemedi: {{errorMessage}}", + "checkFailed": "Semble kontrolü başarısız: {{errorMessage}}", + "providerReset": "Semble sağlayıcısı sıfırlandı. Diskteki önbellek bir sonraki yeniden yapılandırmaya kadar korunur." } } diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index c92ebba276..4bd5d53f91 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Lập chỉ mục yêu cầu một thư mục workspace đang mở", "indexingStopped": "Lập chỉ mục đã bị dừng bởi người dùng.", "indexingStoppedPartial": "Lập chỉ mục đã dừng. Dữ liệu chỉ mục một phần được bảo toàn." + }, + "semble": { + "downloadingBinary": "Đang tải xuống tệp nhị phân semble...", + "ready": "Semble đã sẵn sàng. Tìm kiếm được lập chỉ mục tức thì.", + "unsupportedPlatform": "Semble không được hỗ trợ trên nền tảng này ({{platform}}-{{arch}}).", + "downloadFailed": "Tải xuống semble thất bại: {{errorMessage}}", + "checkFailed": "Kiểm tra semble thất bại: {{errorMessage}}", + "providerReset": "Nhà cung cấp Semble đã đặt lại. Bộ nhớ đệm trên đĩa vẫn còn cho đến lần xây dựng lại tiếp theo." } } diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index b4f4eaad1d..cb9637efec 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "索引需要打开的工作区文件夹", "indexingStopped": "用户已停止索引。", "indexingStoppedPartial": "索引已停止。部分索引数据已保留。" + }, + "semble": { + "downloadingBinary": "正在下载 semble 二进制文件...", + "ready": "Semble 已就绪。搜索即时建立索引。", + "unsupportedPlatform": "此平台不支持 Semble ({{platform}}-{{arch}})。", + "downloadFailed": "下载 semble 失败:{{errorMessage}}", + "checkFailed": "Semble 检查失败:{{errorMessage}}", + "providerReset": "Semble 提供程序已重置。磁盘缓存将保留至下次重建。" } } diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 26845ed948..5f1893d465 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "索引需要開啟的工作區資料夾", "indexingStopped": "使用者已停止索引。", "indexingStoppedPartial": "索引已停止。部分索引資料已保留。" + }, + "semble": { + "downloadingBinary": "正在下載 semble 二進位檔...", + "ready": "Semble 已就緒。搜尋即時建立索引。", + "unsupportedPlatform": "此平台不支援 Semble ({{platform}}-{{arch}})。", + "downloadFailed": "下載 semble 失敗:{{errorMessage}}", + "checkFailed": "Semble 檢查失敗:{{errorMessage}}", + "providerReset": "Semble 提供者已重設。磁碟快取將保留至下次重建。" } } diff --git a/src/services/code-index/semble/__tests__/provider.spec.ts b/src/services/code-index/semble/__tests__/provider.spec.ts index 5de0d6c5e7..4733d7b790 100644 --- a/src/services/code-index/semble/__tests__/provider.spec.ts +++ b/src/services/code-index/semble/__tests__/provider.spec.ts @@ -37,6 +37,29 @@ vi.mock("vscode", () => ({ ExtensionContext: vi.fn(), })) +// Mock i18n — semble provider state messages are internationalized via t(). +// Return the English strings so existing message-content assertions stay stable. +vi.mock("../../../../i18n", () => ({ + t: (key: string, params?: any) => { + switch (key) { + case "embeddings:semble.downloadingBinary": + return "Downloading semble binary..." + case "embeddings:semble.ready": + return "Semble is ready. Searches index on-the-fly." + case "embeddings:semble.unsupportedPlatform": + return `Semble is not supported on this platform (${params?.platform ?? ""}-${params?.arch ?? ""}).` + case "embeddings:semble.downloadFailed": + return `Failed to download semble: ${params?.errorMessage ?? ""}` + case "embeddings:semble.checkFailed": + return `Semble check failed: ${params?.errorMessage ?? ""}` + case "embeddings:semble.providerReset": + return "Semble provider reset. On-disk cache remains until next rebuild." + default: + return key + } + }, +})) + import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" import { isSembleSupportedPlatform, downloadSemble } from "../semble-downloader" @@ -194,25 +217,17 @@ describe("SembleProvider", () => { it("should search using CLI and convert results", async () => { const mockResults = [ { - chunk: { - content: "function authenticate() {}", - file_path: "src/auth.ts", - start_line: 10, - end_line: 25, - language: "typescript", - location: "src/auth.ts:10-25", - }, + content: "function authenticate() {}", + file_path: "src/auth.ts", + start_line: 10, + end_line: 25, score: 0.92, }, { - chunk: { - content: "export function login() {}", - file_path: "src/login.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/login.ts:5-15", - }, + content: "export function login() {}", + file_path: "src/login.ts", + start_line: 5, + end_line: 15, score: 0.78, }, ] @@ -252,36 +267,24 @@ describe("SembleProvider", () => { it("should filter out results with missing file_path", async () => { const mockResults = [ { - chunk: { - content: "good result", - file_path: "src/good.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/good.ts:1-10", - }, + content: "good result", + file_path: "src/good.ts", + start_line: 1, + end_line: 10, score: 0.8, }, { - chunk: { - content: "no file path result", - file_path: "", - start_line: 1, - end_line: 5, - language: "typescript", - location: "", - }, + content: "no file path result", + file_path: "", + start_line: 1, + end_line: 5, score: 0.5, }, { - chunk: { - content: "null file path result", - file_path: null, - start_line: 1, - end_line: 5, - language: null, - location: "", - }, + content: "null file path result", + file_path: null, + start_line: 1, + end_line: 5, score: 0.3, }, ] @@ -321,36 +324,24 @@ describe("SembleProvider", () => { it("should filter results by directoryPrefix when provided", async () => { const mockResults = [ { - chunk: { - content: "code in src/auth", - file_path: "src/auth/login.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/auth/login.ts:1-10", - }, + content: "code in src/auth", + file_path: "src/auth/login.ts", + start_line: 1, + end_line: 10, score: 0.95, }, { - chunk: { - content: "code in src/utils", - file_path: "src/utils/helper.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/utils/helper.ts:5-15", - }, + content: "code in src/utils", + file_path: "src/utils/helper.ts", + start_line: 5, + end_line: 15, score: 0.8, }, { - chunk: { - content: "code in root", - file_path: "README.md", - start_line: 1, - end_line: 5, - language: "markdown", - location: "README.md:1-5", - }, + content: "code in root", + file_path: "README.md", + start_line: 1, + end_line: 5, score: 0.6, }, ] @@ -367,25 +358,17 @@ describe("SembleProvider", () => { it("should not filter results when no directoryPrefix is provided", async () => { const mockResults = [ { - chunk: { - content: "code in src/auth", - file_path: "src/auth/login.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/auth/login.ts:1-10", - }, + content: "code in src/auth", + file_path: "src/auth/login.ts", + start_line: 1, + end_line: 10, score: 0.95, }, { - chunk: { - content: "code in src/utils", - file_path: "src/utils/helper.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/utils/helper.ts:5-15", - }, + content: "code in src/utils", + file_path: "src/utils/helper.ts", + start_line: 5, + end_line: 15, score: 0.8, }, ] @@ -459,14 +442,10 @@ describe("SembleProvider", () => { it("should handle results with null content using empty string fallback", async () => { const mockResults = [ { - chunk: { - content: null, - file_path: "src/file.ts", - start_line: null, - end_line: null, - language: null, - location: "", - }, + content: null, + file_path: "src/file.ts", + start_line: null, + end_line: null, score: 0.6, }, ] @@ -484,14 +463,10 @@ describe("SembleProvider", () => { it("should handle results with undefined content fields", async () => { const mockResults = [ { - chunk: { - content: undefined, - file_path: "src/file.ts", - start_line: undefined, - end_line: undefined, - language: undefined, - location: "", - }, + content: undefined, + file_path: "src/file.ts", + start_line: undefined, + end_line: undefined, score: 0.5, }, ] @@ -509,14 +484,10 @@ describe("SembleProvider", () => { it("should normalize backslashes in file paths", async () => { const mockResults = [ { - chunk: { - content: "code", - file_path: "src\\nested\\file.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "", - }, + content: "code", + file_path: "src\\nested\\file.ts", + start_line: 1, + end_line: 10, score: 0.8, }, ] @@ -533,14 +504,10 @@ describe("SembleProvider", () => { it("should always join file paths against workspace root, even with directoryPrefix", async () => { const mockResults = [ { - chunk: { - content: "code", - file_path: "src/file.ts", - start_line: 1, - end_line: 5, - language: "typescript", - location: "", - }, + content: "code", + file_path: "src/file.ts", + start_line: 1, + end_line: 5, score: 0.9, }, ] @@ -556,36 +523,24 @@ describe("SembleProvider", () => { it("should assign sequential semble-N IDs to results", async () => { const mockResults = [ { - chunk: { - content: "a", - file_path: "a.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "a", + file_path: "a.ts", + start_line: 1, + end_line: 2, score: 0.9, }, { - chunk: { - content: "b", - file_path: "b.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "b", + file_path: "b.ts", + start_line: 1, + end_line: 2, score: 0.8, }, { - chunk: { - content: "c", - file_path: "c.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "c", + file_path: "c.ts", + start_line: 1, + end_line: 2, score: 0.7, }, ] diff --git a/src/services/code-index/semble/__tests__/semble-cli.spec.ts b/src/services/code-index/semble/__tests__/semble-cli.spec.ts index 5f692969f7..e9b7fd4594 100644 --- a/src/services/code-index/semble/__tests__/semble-cli.spec.ts +++ b/src/services/code-index/semble/__tests__/semble-cli.spec.ts @@ -219,14 +219,10 @@ describe("SembleCLI", () => { query: "related", results: [ { - chunk: { - content: "related code", - file_path: "src/related.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/related.ts:1-10", - }, + content: "related code", + file_path: "src/related.ts", + start_line: 1, + end_line: 10, score: 0.85, }, ], @@ -236,36 +232,28 @@ describe("SembleCLI", () => { const results = await cli.findRelated("src/auth.ts", 42, "/repo") expect(results).toHaveLength(1) - expect(results[0].chunk.file_path).toBe("src/related.ts") + expect(results[0].file_path).toBe("src/related.ts") expect(results[0].score).toBe(0.85) }) }) describe("_parseOutput (via search)", () => { - it("should parse v0.3.0+ JSON format with nested chunk", async () => { + it("should parse v0.4.0+ flat JSON format (no chunk wrapper)", async () => { const jsonResponse = { query: "authentication", results: [ { - chunk: { - content: "function authenticate() {}", - file_path: "src/auth.ts", - start_line: 10, - end_line: 25, - language: "typescript", - location: "src/auth.ts:10-25", - }, + content: "function authenticate() {}", + file_path: "src/auth.ts", + start_line: 10, + end_line: 25, score: 0.92, }, { - chunk: { - content: "export function login() {}", - file_path: "src/login.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/login.ts:5-15", - }, + content: "export function login() {}", + file_path: "src/login.ts", + start_line: 5, + end_line: 15, score: 0.78, }, ], @@ -276,12 +264,12 @@ describe("SembleCLI", () => { const results = await cli.search("authentication", "/repo") expect(results).toHaveLength(2) - expect(results[0].chunk.file_path).toBe("src/auth.ts") - expect(results[0].chunk.start_line).toBe(10) - expect(results[0].chunk.end_line).toBe(25) - expect(results[0].chunk.content).toBe("function authenticate() {}") + expect(results[0].file_path).toBe("src/auth.ts") + expect(results[0].start_line).toBe(10) + expect(results[0].end_line).toBe(25) + expect(results[0].content).toBe("function authenticate() {}") expect(results[0].score).toBe(0.92) - expect(results[1].chunk.file_path).toBe("src/login.ts") + expect(results[1].file_path).toBe("src/login.ts") expect(results[1].score).toBe(0.78) }) @@ -325,17 +313,13 @@ describe("SembleCLI", () => { expect(results).toEqual([]) }) - it("should handle flat array format (older semble format)", async () => { + it("should handle flat array format (bare array of result entries)", async () => { const flatArray = [ { - chunk: { - content: "old format result", - file_path: "src/old.ts", - start_line: 1, - end_line: 5, - language: "typescript", - location: "src/old.ts:1-5", - }, + content: "flat array result", + file_path: "src/old.ts", + start_line: 1, + end_line: 5, score: 0.7, }, ] @@ -344,7 +328,7 @@ describe("SembleCLI", () => { const results = await cli.search("test", "/repo") expect(results).toHaveLength(1) - expect(results[0].chunk.file_path).toBe("src/old.ts") + expect(results[0].file_path).toBe("src/old.ts") expect(results[0].score).toBe(0.7) }) diff --git a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts index 210abf3e32..3a16d3dc0f 100644 --- a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts +++ b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts @@ -7,10 +7,10 @@ import { EventEmitter } from "events" // and computes a SHA-256. We make digest() dynamically return the expected checksum // for the current process.platform/arch so verification always passes in unit tests. const CHECKSUMS: Record = { - "linux-x64": "2bd4117dbd1ff7a26ed5ef44dad8d43162a4b9f431ec0bcc9dd2f9c6f5952e28", - "linux-arm64": "177d14f41d3272594844a2635d59d97ad20400868a874a59169fd26a868c32a5", - "darwin-arm64": "9130f447ff2c21803853a9aee58268f0e05134326384ac23d8b74ed22905e118", - "win32-x64": "c8ae86f3703675e356824e08cf79c8a20c41c602296d2a5bff15bf35d762a46b", + "linux-x64": "33a6c8ae78d750e917b291524d788747c62de795274def5c6b07b7a6d1671493", + "linux-arm64": "a4a3fbca363f5a894a57594679c787ff6b4ac1332ebf0edcb36cc89f348c7aba", + "darwin-arm64": "f8b5718e2264c9addbf61ac52f0106f1ebb6717980bf25ecfe135d12f164ed30", + "win32-x64": "2a8734d486db1feaa3bd3cf111d1ac17c805102d758be8f5295fbc862ee00bb3", } vi.mock("crypto", () => ({ createHash: vi.fn(() => ({ @@ -29,6 +29,7 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn(), writeFile: vi.fn().mockResolvedValue(undefined), rename: vi.fn().mockResolvedValue(undefined), + readdir: vi.fn().mockResolvedValue([]), })) // Mock fs (createWriteStream and createReadStream for checksum verification) @@ -182,7 +183,7 @@ describe("semble-downloader", () => { // fs.access resolves => file exists ;(fs.access as any).mockResolvedValue(undefined) // Version file matches current version - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") try { const result = await downloadSemble("/storage") @@ -231,7 +232,7 @@ describe("semble-downloader", () => { "tar", [ "-xzf", - path.join("/storage", "semble-linux-x64-fast.tar.gz"), + path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz"), "-C", path.join("/storage", "semble.new"), "--no-same-owner", @@ -248,11 +249,11 @@ describe("semble-downloader", () => { // Version file should be written expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) - // Archive should be cleaned up - expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // Archive should be cleaned up (version-prefixed local cache path) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz")) } finally { if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) if (originalArch) Object.defineProperty(process, "arch", originalArch) @@ -269,7 +270,7 @@ describe("semble-downloader", () => { // fs.access resolves => file exists ;(fs.access as any).mockResolvedValue(undefined) // Version file matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") try { const result = await downloadSemble("/storage") @@ -309,7 +310,7 @@ describe("semble-downloader", () => { try { await expect(downloadSemble("/storage")).rejects.toThrow("Failed to download semble") - expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-arm64-fast.tar.gz")) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-arm64-fast.tar.gz")) // Should clean up staging directory, not the original expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "semble.new"), { recursive: true, @@ -622,11 +623,64 @@ describe("semble-downloader", () => { path.join("/storage", "semble"), ) // Should download the new version - expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.3.1"), expect.any(Function)) + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) // Should write the new version file expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", + "utf-8", + ) + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + + it("should download a fresh package immediately after a version upgrade (version-prefixed archive path)", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // Old version recorded on disk → must trigger a fresh download + ;(fs.readFile as any).mockResolvedValue("v0.4.0") + // fs.access resolves — only called for staged binary verification + ;(fs.access as any).mockResolvedValue(undefined) + + // Simulate successful download + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + const result = await downloadSemble("/storage") + + expect(result).toBe(path.join("/storage", "semble", "semble")) + const versionedArchive = path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz") + + // A fresh download must happen after the version upgrade + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) + // The release URL keeps the unversioned asset name + expect(https.get).toHaveBeenCalledWith( + expect.stringContaining("semble-linux-x64-fast.tar.gz"), + expect.any(Function), + ) + // Extraction reads from the version-prefixed local cache path + expect(spawn).toHaveBeenCalledWith( + "tar", + expect.arrayContaining(["-xzf", versionedArchive]), + expect.any(Object), + ) + // The stale archive is removed before the fresh download to guarantee + // a clean package is verified against the new checksum. + expect(fs.unlink).toHaveBeenCalledWith(versionedArchive) + // The new version file is recorded + expect(fs.writeFile).toHaveBeenCalledWith( + path.join("/storage", "semble", ".semble-version"), + "v0.4.1", "utf-8", ) } finally { @@ -643,7 +697,7 @@ describe("semble-downloader", () => { Object.defineProperty(process, "arch", { value: "x64", configurable: true }) // Version matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") // Binary exists ;(fs.access as any).mockResolvedValue(undefined) @@ -669,7 +723,7 @@ describe("semble-downloader", () => { Object.defineProperty(process, "arch", { value: "x64", configurable: true }) // Version matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") // But binary is missing let accessCallCount = 0 ;(fs.access as any).mockImplementation(() => { @@ -702,7 +756,7 @@ describe("semble-downloader", () => { // Should write version file again expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) } finally { @@ -743,7 +797,7 @@ describe("semble-downloader", () => { // Should write version file expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) } finally { diff --git a/src/services/code-index/semble/index.ts b/src/services/code-index/semble/index.ts index e63115e076..dc3a86b3d0 100644 --- a/src/services/code-index/semble/index.ts +++ b/src/services/code-index/semble/index.ts @@ -6,12 +6,5 @@ export { downloadSemble, getSembleBinaryPath, } from "./semble-downloader" -export type { - ISembleProvider, - SembleSearchResult, - SembleChunk, - SembleCheckResult, - SembleConfig, - SembleContentType, -} from "./types" +export type { ISembleProvider, SembleSearchResult, SembleCheckResult, SembleConfig, SembleContentType } from "./types" export { SEMBLE_DEFAULTS } from "./types" diff --git a/src/services/code-index/semble/provider.ts b/src/services/code-index/semble/provider.ts index 176169814a..42e3dc567c 100644 --- a/src/services/code-index/semble/provider.ts +++ b/src/services/code-index/semble/provider.ts @@ -9,6 +9,7 @@ import { downloadSemble, isSembleSupportedPlatform } from "./semble-downloader" import { ISembleProvider, SembleConfig, SembleContentType, SembleSearchResult, SEMBLE_DEFAULTS } from "./types" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import { t } from "../../../i18n" /** * Orchestrates code search via the semble CLI. @@ -82,7 +83,7 @@ export class SembleProvider implements ISembleProvider { this._state = "Error" this.stateManager.setSystemState( "Error", - `Semble is not supported on this platform (${process.platform}-${process.arch}).`, + t("embeddings:semble.unsupportedPlatform", { platform: process.platform, arch: process.arch }), ) console.error(`[SembleProvider] Unsupported platform: ${process.platform}-${process.arch}`) return @@ -90,7 +91,7 @@ export class SembleProvider implements ISembleProvider { // Download semble binary try { - this.stateManager.setSystemState("Indexing", "Downloading semble binary...") + this.stateManager.setSystemState("Indexing", t("embeddings:semble.downloadingBinary")) const storageDir = this.context.globalStorageUri.fsPath const binaryPath = await downloadSemble(storageDir) if (!binaryPath) { @@ -99,7 +100,10 @@ export class SembleProvider implements ISembleProvider { this.cli = new SembleCLI(binaryPath) } catch (error: any) { this._state = "Error" - this.stateManager.setSystemState("Error", `Failed to download semble: ${error?.message || error}`) + this.stateManager.setSystemState( + "Error", + t("embeddings:semble.downloadFailed", { errorMessage: error?.message || error }), + ) console.error("[SembleProvider] Download failed:", error?.message || error) return } @@ -110,7 +114,7 @@ export class SembleProvider implements ISembleProvider { if (!checkResult.installed) { const errorMsg = checkResult.error || "Semble binary is not functional" this._state = "Error" - this.stateManager.setSystemState("Error", `Semble check failed: ${errorMsg}`) + this.stateManager.setSystemState("Error", t("embeddings:semble.checkFailed", { errorMessage: errorMsg })) console.error("[SembleProvider] Semble check failed:", errorMsg) return } @@ -119,7 +123,7 @@ export class SembleProvider implements ISembleProvider { // Semble indexes on-the-fly, so we mark as "Indexed" (ready for search) this._state = "Indexed" - this.stateManager.setSystemState("Indexed", "Semble is ready. Searches index on-the-fly.") + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready")) this._isInitialized = true } @@ -140,7 +144,7 @@ export class SembleProvider implements ISembleProvider { // Semble indexes on-the-fly — no separate indexing step needed. // Mark as indexed/ready. this._state = "Indexed" - this.stateManager.setSystemState("Indexed", "Semble is ready. Searches index on-the-fly.") + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready")) } /** @@ -220,7 +224,7 @@ export class SembleProvider implements ISembleProvider { */ async clearIndexData(): Promise { this._state = "Standby" - this.stateManager.setSystemState("Standby", "Semble provider reset. On-disk cache remains until next rebuild.") + this.stateManager.setSystemState("Standby", t("embeddings:semble.providerReset")) } /** @@ -235,8 +239,8 @@ export class SembleProvider implements ISembleProvider { /** * Converts Semble CLI results to Zoo's VectorStoreSearchResult format. * - * Semble v0.3.0+ returns results in the format: - * { chunk: { content, file_path, start_line, end_line, language, location }, score } + * Semble v0.4.0+ returns results in a flat format (no `chunk` wrapper): + * { file_path, start_line, end_line, score, content } * * Note: semble returns file paths relative to the path it was invoked with. * We join against `basePath` (the actual path passed to semble) to produce @@ -250,15 +254,15 @@ export class SembleProvider implements ISembleProvider { const converted: VectorStoreSearchResult[] = [] for (const [index, r] of results.entries()) { - if (!r.chunk?.file_path) { + if (!r.file_path) { continue } // Use path.join for the displayed path (preserves basePath format). - const filePath = path.join(basePath, r.chunk.file_path).replace(/\\/g, "/") + const filePath = path.join(basePath, r.file_path).replace(/\\/g, "/") // Use path.resolve to normalize any ../ for the security check. - const resolvedFilePath = path.resolve(basePath, r.chunk.file_path).replace(/\\/g, "/") + const resolvedFilePath = path.resolve(basePath, r.file_path).replace(/\\/g, "/") // Guard against path traversal: reject file paths that resolve outside the workspace if (!resolvedFilePath.startsWith(resolvedBase + "/") && resolvedFilePath !== resolvedBase) { @@ -270,9 +274,9 @@ export class SembleProvider implements ISembleProvider { score: r.score, payload: { filePath, - codeChunk: r.chunk?.content ?? "", - startLine: r.chunk?.start_line ?? 0, - endLine: r.chunk?.end_line ?? 0, + codeChunk: r.content ?? "", + startLine: r.start_line ?? 0, + endLine: r.end_line ?? 0, }, }) } diff --git a/src/services/code-index/semble/semble-cli.ts b/src/services/code-index/semble/semble-cli.ts index 51b9f2e348..572e8b1581 100644 --- a/src/services/code-index/semble/semble-cli.ts +++ b/src/services/code-index/semble/semble-cli.ts @@ -10,15 +10,17 @@ import { SembleSearchResult, SembleCheckResult, SembleContentType, SEMBLE_DEFAUL * All methods spawn the semble process via child_process.spawn with array * arguments (no shell) to prevent shell injection. * - * Semble CLI (v0.3.0+) subcommands: + * Semble CLI (v0.4.0+) subcommands: * search [path] — search a codebase * find-related [path] — find similar code - * init — write sub-agent file + * clear [all|index|savings] — clear cached indexes/savings + * install / uninstall — configure coding-agent integrations * savings — show token stats * * Common flags: * -k, --top-k N — number of results (default: 5) * --content TYPE [TYPE ...] — content types: code, docs, config, all + * --max-snippet-lines N — lines of source per result (default: full chunk) */ export class SembleCLI { private readonly semblePath: string @@ -167,8 +169,9 @@ export class SembleCLI { /** * Parses semble CLI JSON output into structured results. * - * Semble v0.3.0+ outputs JSON by default with format: - * { "query": "...", "results": [{ "chunk": { "content": "...", "file_path": "...", "start_line": N, "end_line": M, "language": "...", "location": "..." }, "score": X }] } + * Semble v0.4.0+ outputs JSON by default with a flat format (no `chunk` + * wrapper — the chunk fields are top-level on each result entry): + * { "query": "...", "results": [{ "file_path": "...", "start_line": N, "end_line": M, "score": X, "content": "..." }] } * * If the query returns no results, semble outputs: * { "error": "No results found." } @@ -187,19 +190,19 @@ export class SembleCLI { return [] } - // Handle successful response: {query, results: [{chunk, score}]} + // Handle successful response: {query, results: [{file_path, start_line, end_line, score, content}]} if (parsed.results && Array.isArray(parsed.results)) { return parsed.results as SembleSearchResult[] } - // Fallback: if it's a flat array (older format) + // Fallback: if it's a flat array of result entries if (Array.isArray(parsed)) { return parsed as SembleSearchResult[] } return [] } catch { - // Not JSON — this shouldn't happen with v0.3.0+ but handle gracefully + // Not JSON — this shouldn't happen with v0.4.0+ but handle gracefully console.warn("[SembleCLI] Unexpected non-JSON output from semble") return [] } diff --git a/src/services/code-index/semble/semble-downloader.ts b/src/services/code-index/semble/semble-downloader.ts index a8b08abee5..a368252578 100644 --- a/src/services/code-index/semble/semble-downloader.ts +++ b/src/services/code-index/semble/semble-downloader.ts @@ -20,7 +20,7 @@ const SEMBLE_ARCHIVES: Record = { "win32-x64": { archive: "semble-windows-x64-fast.zip", binary: "semble.exe" }, } -const SEMBLE_VERSION = "v0.3.1" +const SEMBLE_VERSION = "v0.4.1" const DOWNLOAD_BASE_URL = `https://github.com/Zoo-Code-Org/sembleexec/releases/download/${SEMBLE_VERSION}` const VERSION_FILE = ".semble-version" @@ -32,10 +32,10 @@ const VERSION_FILE = ".semble-version" * To regenerate: `shasum -a 256 ` */ const SEMBLE_SHA256: Record = { - "linux-x64": "2bd4117dbd1ff7a26ed5ef44dad8d43162a4b9f431ec0bcc9dd2f9c6f5952e28", - "linux-arm64": "177d14f41d3272594844a2635d59d97ad20400868a874a59169fd26a868c32a5", - "darwin-arm64": "9130f447ff2c21803853a9aee58268f0e05134326384ac23d8b74ed22905e118", - "win32-x64": "c8ae86f3703675e356824e08cf79c8a20c41c602296d2a5bff15bf35d762a46b", + "linux-x64": "33a6c8ae78d750e917b291524d788747c62de795274def5c6b07b7a6d1671493", + "linux-arm64": "a4a3fbca363f5a894a57594679c787ff6b4ac1332ebf0edcb36cc89f348c7aba", + "darwin-arm64": "f8b5718e2264c9addbf61ac52f0106f1ebb6717980bf25ecfe135d12f164ed30", + "win32-x64": "2a8734d486db1feaa3bd3cf111d1ac17c805102d758be8f5295fbc862ee00bb3", } /** @@ -105,6 +105,32 @@ async function writeInstalledVersion(storageDir: string, version: string): Promi await fs.writeFile(versionPath, version, "utf-8") } +/** + * Best-effort removal of archive files left over from previous semble versions. + * + * Because the local archive cache path is version-prefixed (see `downloadSemble`), + * upgrading SEMBLE_VERSION leaves the prior version's archive orphaned on disk. + * This sweeps those stale packages so a version upgrade doesn't accumulate them. + * Errors are swallowed since this is purely cosmetic cleanup. + */ +async function cleanupStaleArchives( + storageDir: string, + archiveName: string, + currentArchivePath: string, +): Promise { + try { + const entries = await fs.readdir(storageDir) + const suffix = `-${archiveName}` + await Promise.all( + entries + .filter((name) => name.endsWith(suffix) && path.join(storageDir, name) !== currentArchivePath) + .map((name) => fs.unlink(path.join(storageDir, name)).catch(() => {})), + ) + } catch { + // ignore — storage dir may not be listable yet + } +} + /** * Downloads and extracts the semble archive for the current platform. * @@ -152,7 +178,12 @@ export async function downloadSemble(storageDir: string): Promise Date: Fri, 26 Jun 2026 19:02:54 -0700 Subject: [PATCH 2/3] fix(semble): clean legacy unversioned archives and cover stale-archive sweep CodeRabbit PR #734 follow-ups: - cleanupStaleArchives now also matches the exact unversioned archive name (pre-v0.4.0 cache layout) in addition to the version-prefixed suffix, so a v0.3.1 -> v0.4.1 upgrade also clears the legacy file. The current archive path is still preserved. - semble-downloader.spec: the version-upgrade test now simulates a prior-version archive (v0.4.0-*) and a legacy unversioned archive in the cache, and asserts both are swept during the upgrade flow. - Add coverage for the cleanupStaleArchives catch block (readdir rejects) and for the current-archive/unrelated-file preservation behavior. - provider.spec: add a test that rejects search with a non-Error value to cover the 'error instanceof Error' false branch of the telemetry payload (stack: undefined), raising provider.ts patch coverage. --- .../semble/__tests__/provider.spec.ts | 18 ++++ .../__tests__/semble-downloader.spec.ts | 98 +++++++++++++++++++ .../code-index/semble/semble-downloader.ts | 12 ++- 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/services/code-index/semble/__tests__/provider.spec.ts b/src/services/code-index/semble/__tests__/provider.spec.ts index 4733d7b790..b44854883f 100644 --- a/src/services/code-index/semble/__tests__/provider.spec.ts +++ b/src/services/code-index/semble/__tests__/provider.spec.ts @@ -395,6 +395,24 @@ describe("SembleProvider", () => { ) }) + it("should capture stack only when error is an Error instance", async () => { + // A non-Error rejection exercises the `error instanceof Error` false + // branch of the telemetry payload (stack: undefined). + mockCli.search.mockRejectedValue("string error") + + const results = await provider.searchIndex("test") + + expect(results).toEqual([]) + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.CODE_INDEX_ERROR, + expect.objectContaining({ + error: "string error", + stack: undefined, + location: "SembleProvider.searchIndex", + }), + ) + }) + it("should return empty array when in Error state", async () => { ;(isSembleSupportedPlatform as any).mockReturnValue(false) const errorProvider = new SembleProvider("/workspace", mockContext, mockStateManager) diff --git a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts index 3a16d3dc0f..b28051bf91 100644 --- a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts +++ b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts @@ -645,6 +645,15 @@ describe("semble-downloader", () => { // Old version recorded on disk → must trigger a fresh download ;(fs.readFile as any).mockResolvedValue("v0.4.0") + // Simulate a prior-version archive (v0.4.0) and a legacy unversioned + // archive (pre-v0.4.0 cache layout) left over in the storage dir. + // cleanupStaleArchives must sweep both during the upgrade. + ;(fs.readdir as any).mockResolvedValue([ + "v0.4.0-semble-linux-x64-fast.tar.gz", + "semble-linux-x64-fast.tar.gz", + "v0.4.1-semble-linux-x64-fast.tar.gz", + "unrelated-file.txt", + ]) // fs.access resolves — only called for staged binary verification ;(fs.access as any).mockResolvedValue(undefined) @@ -677,6 +686,15 @@ describe("semble-downloader", () => { // The stale archive is removed before the fresh download to guarantee // a clean package is verified against the new checksum. expect(fs.unlink).toHaveBeenCalledWith(versionedArchive) + // The prior-version archive (v0.4.0-*) is swept by cleanupStaleArchives + // after a successful install, so a version upgrade doesn't accumulate + // orphaned packages on disk. + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz")) + // The legacy unversioned archive (pre-v0.4.0 cache layout) is also + // swept, covering the v0.3.1 → v0.4.1 upgrade path. + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // Unrelated files in the storage dir must not be touched. + expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated-file.txt")) // The new version file is recorded expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), @@ -806,4 +824,84 @@ describe("semble-downloader", () => { } }) }) + + describe("downloadSemble - stale archive cleanup", () => { + it("should ignore readdir failures during stale-archive cleanup", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // First install (no version file) → triggers a fresh download + ;(fs.readFile as any).mockRejectedValue(new Error("ENOENT")) + ;(fs.access as any).mockResolvedValue(undefined) + // readdir rejects — exercises the catch block in cleanupStaleArchives + ;(fs.readdir as any).mockRejectedValue(new Error("EACCES")) + + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + const result = await downloadSemble("/storage") + + // Should still succeed — cleanup failure is swallowed + expect(result).toBe(path.join("/storage", "semble", "semble")) + expect(fs.readdir).toHaveBeenCalledWith("/storage") + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + + it("should preserve the current archive and unrelated files during cleanup", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // First install (no version file) → triggers a fresh download + ;(fs.readFile as any).mockRejectedValue(new Error("ENOENT")) + ;(fs.access as any).mockResolvedValue(undefined) + // Storage dir contains the current archive plus unrelated files + ;(fs.readdir as any).mockResolvedValue([ + "v0.4.1-semble-linux-x64-fast.tar.gz", + "v0.4.0-semble-linux-x64-fast.tar.gz", + "semble-linux-x64-fast.tar.gz", + "unrelated.txt", + ]) + + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + await downloadSemble("/storage") + + const currentArchive = path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz") + // Stale versioned + legacy unversioned archives are swept + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz")) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // The current archive is never swept by cleanupStaleArchives (it is + // excluded by the currentArchivePath guard). It is unlinked only by + // the pre-download partial-archive cleanup and the post-install + // archive cleanup steps. unrelated.txt is never touched. + expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated.txt")) + // Sanity: the current archive path is never passed to the stale sweep. + // It is unlinked exactly twice (pre-download cleanup + post-install + // archive cleanup), never via cleanupStaleArchives. + const currentUnlinks = (fs.unlink as any).mock.calls.filter((c: any[]) => c[0] === currentArchive) + expect(currentUnlinks.length).toBe(2) + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + }) }) diff --git a/src/services/code-index/semble/semble-downloader.ts b/src/services/code-index/semble/semble-downloader.ts index a368252578..3245b82db9 100644 --- a/src/services/code-index/semble/semble-downloader.ts +++ b/src/services/code-index/semble/semble-downloader.ts @@ -111,6 +111,12 @@ async function writeInstalledVersion(storageDir: string, version: string): Promi * Because the local archive cache path is version-prefixed (see `downloadSemble`), * upgrading SEMBLE_VERSION leaves the prior version's archive orphaned on disk. * This sweeps those stale packages so a version upgrade doesn't accumulate them. + * + * Matches both the version-prefixed cache names (`${version}-${archiveName}`, + * used since v0.4.0) and the legacy unversioned cache name (`${archiveName}`, + * used before v0.4.0), so a v0.3.1 → v0.4.1 upgrade also clears the legacy file. + * The current archive path is always preserved. + * * Errors are swallowed since this is purely cosmetic cleanup. */ async function cleanupStaleArchives( @@ -123,7 +129,11 @@ async function cleanupStaleArchives( const suffix = `-${archiveName}` await Promise.all( entries - .filter((name) => name.endsWith(suffix) && path.join(storageDir, name) !== currentArchivePath) + .filter( + (name) => + (name === archiveName || name.endsWith(suffix)) && + path.join(storageDir, name) !== currentArchivePath, + ) .map((name) => fs.unlink(path.join(storageDir, name)).catch(() => {})), ) } catch { From 34eea6a55ebe7472906684eb56cf02c719af80cd Mon Sep 17 00:00:00 2001 From: Naved Merchant Date: Fri, 26 Jun 2026 19:18:55 -0700 Subject: [PATCH 3/3] feat(semble): surface active version in the CodeIndexPopover status message Export SEMBLE_VERSION from semble-downloader and re-export from the semble barrel. SembleProvider now interpolates the active version into the 'embeddings:semble.ready' system-state message (set in both _doInitialize and startIndexing), so the CodeIndexPopover status line shows e.g. 'Indexed - Semble v0.4.1 is ready. Searches index on-the-fly.' - semble-downloader.ts: export SEMBLE_VERSION. - index.ts: re-export SEMBLE_VERSION. - provider.ts: pass { version: SEMBLE_VERSION } to t('embeddings:semble.ready'). - i18n: update semble.ready across all 18 locales to include {{version}}. - provider.spec.ts: mock SEMBLE_VERSION, update the ready-message mock to interpolate version, and add a test asserting the version appears in the ready status message. --- src/i18n/locales/ca/embeddings.json | 2 +- src/i18n/locales/de/embeddings.json | 2 +- src/i18n/locales/en/embeddings.json | 2 +- src/i18n/locales/es/embeddings.json | 2 +- src/i18n/locales/fr/embeddings.json | 2 +- src/i18n/locales/hi/embeddings.json | 2 +- src/i18n/locales/id/embeddings.json | 2 +- src/i18n/locales/it/embeddings.json | 2 +- src/i18n/locales/ja/embeddings.json | 2 +- src/i18n/locales/ko/embeddings.json | 2 +- src/i18n/locales/nl/embeddings.json | 2 +- src/i18n/locales/pl/embeddings.json | 2 +- src/i18n/locales/pt-BR/embeddings.json | 2 +- src/i18n/locales/ru/embeddings.json | 2 +- src/i18n/locales/tr/embeddings.json | 2 +- src/i18n/locales/vi/embeddings.json | 2 +- src/i18n/locales/zh-CN/embeddings.json | 2 +- src/i18n/locales/zh-TW/embeddings.json | 2 +- .../code-index/semble/__tests__/provider.spec.ts | 15 +++++++++++++-- src/services/code-index/semble/index.ts | 1 + src/services/code-index/semble/provider.ts | 10 ++++++---- .../code-index/semble/semble-downloader.ts | 6 +++++- 22 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 00a8284c58..3075773504 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Descarregant el binari de semble...", - "ready": "Semble està llest. Les cerques s'indexen sobre la marxa.", + "ready": "Semble {{version}} està llest. Les cerques s'indexen sobre la marxa.", "unsupportedPlatform": "Semble no és compatible amb aquesta plataforma ({{platform}}-{{arch}}).", "downloadFailed": "Error en descarregar semble: {{errorMessage}}", "checkFailed": "Comprovació de semble fallida: {{errorMessage}}", diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index a52d43bbc1..4209c80a16 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Semble-Binary wird heruntergeladen...", - "ready": "Semble ist bereit. Suchen erfolgen spontan.", + "ready": "Semble {{version}} ist bereit. Suchen erfolgen spontan.", "unsupportedPlatform": "Semble wird auf dieser Plattform nicht unterstützt ({{platform}}-{{arch}}).", "downloadFailed": "Fehler beim Herunterladen von Semble: {{errorMessage}}", "checkFailed": "Semble-Überprüfung fehlgeschlagen: {{errorMessage}}", diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 8cbfdf8e8e..36b31a1272 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Downloading semble binary...", - "ready": "Semble is ready. Searches index on-the-fly.", + "ready": "Semble {{version}} is ready. Searches index on-the-fly.", "unsupportedPlatform": "Semble is not supported on this platform ({{platform}}-{{arch}}).", "downloadFailed": "Failed to download semble: {{errorMessage}}", "checkFailed": "Semble check failed: {{errorMessage}}", diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index 345df2e112..8589169f52 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Descargando el binario de semble...", - "ready": "Semble está listo. Las búsquedas se indexan sobre la marcha.", + "ready": "Semble {{version}} está listo. Las búsquedas se indexan sobre la marcha.", "unsupportedPlatform": "Semble no es compatible con esta plataforma ({{platform}}-{{arch}}).", "downloadFailed": "Error al descargar semble: {{errorMessage}}", "checkFailed": "Verificación de semble fallida: {{errorMessage}}", diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index ef9e56f201..33f3d5fbde 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Téléchargement du binaire semble...", - "ready": "Semble est prêt. Les recherches s'indexent à la volée.", + "ready": "Semble {{version}} est prêt. Les recherches s'indexent à la volée.", "unsupportedPlatform": "Semble n'est pas pris en charge sur cette plateforme ({{platform}}-{{arch}}).", "downloadFailed": "Échec du téléchargement de semble : {{errorMessage}}", "checkFailed": "Vérification de semble échouée : {{errorMessage}}", diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 0dc82594b8..e1fc603113 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "semble बाइनरी डाउनलोड हो रहा है...", - "ready": "Semble तैयार है। खोजें ऑन-द-फ्लाई इंडेक्स होती हैं।", + "ready": "Semble {{version}} तैयार है। खोजें ऑन-द-फ्लाई इंडेक्स होती हैं।", "unsupportedPlatform": "इस प्लेटफ़ॉर्म पर Semble समर्थित नहीं है ({{platform}}-{{arch}})।", "downloadFailed": "semble डाउनलोड करने में विफल: {{errorMessage}}", "checkFailed": "Semble जाँच विफल: {{errorMessage}}", diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index 90f3ceadcf..3163e4ff48 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Mengunduh biner semble...", - "ready": "Semble siap. Pencarian diindeks secara langsung.", + "ready": "Semble {{version}} siap. Pencarian diindeks secara langsung.", "unsupportedPlatform": "Semble tidak didukung di platform ini ({{platform}}-{{arch}}).", "downloadFailed": "Gagal mengunduh semble: {{errorMessage}}", "checkFailed": "Pemeriksaan semble gagal: {{errorMessage}}", diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 58f9c8ac80..f49eabb978 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Download del binario semble in corso...", - "ready": "Semble è pronto. Le ricerche vengono indicizzate al volo.", + "ready": "Semble {{version}} è pronto. Le ricerche vengono indicizzate al volo.", "unsupportedPlatform": "Semble non è supportato su questa piattaforma ({{platform}}-{{arch}}).", "downloadFailed": "Download di semble fallito: {{errorMessage}}", "checkFailed": "Verifica di semble fallita: {{errorMessage}}", diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 9f1b27a86a..38526a8ad6 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "sembleバイナリをダウンロード中...", - "ready": "Sembleの準備ができました。検索はその場でインデックス化されます。", + "ready": "Semble {{version}}の準備ができました。検索はその場でインデックス化されます。", "unsupportedPlatform": "このプラットフォームではSembleはサポートされていません ({{platform}}-{{arch}})。", "downloadFailed": "sembleのダウンロードに失敗しました: {{errorMessage}}", "checkFailed": "Sembleの確認に失敗しました: {{errorMessage}}", diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index ad837703cd..94ac0ace73 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "semble 바이너리 다운로드 중...", - "ready": "Semble가 준비되었습니다. 검색은 즉시 인덱싱됩니다.", + "ready": "Semble {{version}}가 준비되었습니다. 검색은 즉시 인덱싱됩니다.", "unsupportedPlatform": "이 플랫폼에서는 Semble가 지원되지 않습니다 ({{platform}}-{{arch}}).", "downloadFailed": "semble 다운로드 실패: {{errorMessage}}", "checkFailed": "Semble 확인 실패: {{errorMessage}}", diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index 3aa562a8e9..da68323b5f 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Semble-binary downloaden...", - "ready": "Semble is klaar. Zoekopdrachten worden direct geïndexeerd.", + "ready": "Semble {{version}} is klaar. Zoekopdrachten worden direct geïndexeerd.", "unsupportedPlatform": "Semble wordt niet ondersteund op dit platform ({{platform}}-{{arch}}).", "downloadFailed": "Downloaden van semble mislukt: {{errorMessage}}", "checkFailed": "Semble-controle mislukt: {{errorMessage}}", diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 1aba2ec5d0..73715fea52 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Pobieranie binariów semble...", - "ready": "Semble jest gotowy. Wyszukiwania są indeksowane w locie.", + "ready": "Semble {{version}} jest gotowy. Wyszukiwania są indeksowane w locie.", "unsupportedPlatform": "Semble nie jest obsługiwany na tej platformie ({{platform}}-{{arch}}).", "downloadFailed": "Nie udało się pobrać semble: {{errorMessage}}", "checkFailed": "Sprawdzenie semble nie powiodło się: {{errorMessage}}", diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index ec13b35dcc..2f4f7eb8d1 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Baixando o binário do semble...", - "ready": "Semble está pronto. As buscas são indexadas em tempo real.", + "ready": "Semble {{version}} está pronto. As buscas são indexadas em tempo real.", "unsupportedPlatform": "Semble não é suportado nesta plataforma ({{platform}}-{{arch}}).", "downloadFailed": "Falha ao baixar semble: {{errorMessage}}", "checkFailed": "Verificação do semble falhou: {{errorMessage}}", diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index b376cef32a..de86730af7 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Загрузка бинарного файла semble...", - "ready": "Semble готов. Поиск индексируется на лету.", + "ready": "Semble {{version}} готов. Поиск индексируется на лету.", "unsupportedPlatform": "Semble не поддерживается на этой платформе ({{platform}}-{{arch}}).", "downloadFailed": "Не удалось загрузить semble: {{errorMessage}}", "checkFailed": "Проверка semble не удалась: {{errorMessage}}", diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index dc33b8f974..37d0595aef 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "semble ikili dosyası indiriliyor...", - "ready": "Semble hazır. Aramalar anında indekslenir.", + "ready": "Semble {{version}} hazır. Aramalar anında indekslenir.", "unsupportedPlatform": "Semble bu platformda desteklenmiyor ({{platform}}-{{arch}}).", "downloadFailed": "semble indirilemedi: {{errorMessage}}", "checkFailed": "Semble kontrolü başarısız: {{errorMessage}}", diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index 4bd5d53f91..0f494674e5 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "Đang tải xuống tệp nhị phân semble...", - "ready": "Semble đã sẵn sàng. Tìm kiếm được lập chỉ mục tức thì.", + "ready": "Semble {{version}} đã sẵn sàng. Tìm kiếm được lập chỉ mục tức thì.", "unsupportedPlatform": "Semble không được hỗ trợ trên nền tảng này ({{platform}}-{{arch}}).", "downloadFailed": "Tải xuống semble thất bại: {{errorMessage}}", "checkFailed": "Kiểm tra semble thất bại: {{errorMessage}}", diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index cb9637efec..484dd7a8e4 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "正在下载 semble 二进制文件...", - "ready": "Semble 已就绪。搜索即时建立索引。", + "ready": "Semble {{version}} 已就绪。搜索即时建立索引。", "unsupportedPlatform": "此平台不支持 Semble ({{platform}}-{{arch}})。", "downloadFailed": "下载 semble 失败:{{errorMessage}}", "checkFailed": "Semble 检查失败:{{errorMessage}}", diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 5f1893d465..66d6328d48 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -75,7 +75,7 @@ }, "semble": { "downloadingBinary": "正在下載 semble 二進位檔...", - "ready": "Semble 已就緒。搜尋即時建立索引。", + "ready": "Semble {{version}} 已就緒。搜尋即時建立索引。", "unsupportedPlatform": "此平台不支援 Semble ({{platform}}-{{arch}})。", "downloadFailed": "下載 semble 失敗:{{errorMessage}}", "checkFailed": "Semble 檢查失敗:{{errorMessage}}", diff --git a/src/services/code-index/semble/__tests__/provider.spec.ts b/src/services/code-index/semble/__tests__/provider.spec.ts index b44854883f..266d5b14f2 100644 --- a/src/services/code-index/semble/__tests__/provider.spec.ts +++ b/src/services/code-index/semble/__tests__/provider.spec.ts @@ -21,6 +21,7 @@ vi.mock("../semble-cli", () => ({ vi.mock("../semble-downloader", () => ({ isSembleSupportedPlatform: vi.fn().mockReturnValue(true), downloadSemble: vi.fn().mockResolvedValue("/mock/storage/semble/semble"), + SEMBLE_VERSION: "v0.4.1", })) // Mock TelemetryService @@ -45,7 +46,7 @@ vi.mock("../../../../i18n", () => ({ case "embeddings:semble.downloadingBinary": return "Downloading semble binary..." case "embeddings:semble.ready": - return "Semble is ready. Searches index on-the-fly." + return `Semble ${params?.version ?? ""} is ready. Searches index on-the-fly.` case "embeddings:semble.unsupportedPlatform": return `Semble is not supported on this platform (${params?.platform ?? ""}-${params?.arch ?? ""}).` case "embeddings:semble.downloadFailed": @@ -113,7 +114,7 @@ describe("SembleProvider", () => { expect(provider.state).toBe("Indexed") expect(mockStateManager.setSystemState).toHaveBeenCalledWith( "Indexed", - "Semble is ready. Searches index on-the-fly.", + "Semble v0.4.1 is ready. Searches index on-the-fly.", ) }) @@ -164,6 +165,16 @@ describe("SembleProvider", () => { expect(mockCli.checkInstalled).toHaveBeenCalledTimes(1) }) + + it("should include the semble version in the ready status message", async () => { + mockCli.checkInstalled.mockResolvedValue({ installed: true }) + + await provider.initialize() + + // The ready message interpolates the active SEMBLE_VERSION so the UI + // (CodeIndexPopover) surfaces which release is installed. + expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Indexed", expect.stringContaining("v0.4.1")) + }) }) describe("startIndexing", () => { diff --git a/src/services/code-index/semble/index.ts b/src/services/code-index/semble/index.ts index dc3a86b3d0..607af2c191 100644 --- a/src/services/code-index/semble/index.ts +++ b/src/services/code-index/semble/index.ts @@ -5,6 +5,7 @@ export { getSembleSupportedPlatforms, downloadSemble, getSembleBinaryPath, + SEMBLE_VERSION, } from "./semble-downloader" export type { ISembleProvider, SembleSearchResult, SembleCheckResult, SembleConfig, SembleContentType } from "./types" export { SEMBLE_DEFAULTS } from "./types" diff --git a/src/services/code-index/semble/provider.ts b/src/services/code-index/semble/provider.ts index 42e3dc567c..1600c03fda 100644 --- a/src/services/code-index/semble/provider.ts +++ b/src/services/code-index/semble/provider.ts @@ -5,7 +5,7 @@ import { IndexingState } from "../interfaces/manager" import { VectorStoreSearchResult } from "../interfaces/vector-store" import { CodeIndexStateManager } from "../state-manager" import { SembleCLI } from "./semble-cli" -import { downloadSemble, isSembleSupportedPlatform } from "./semble-downloader" +import { downloadSemble, isSembleSupportedPlatform, SEMBLE_VERSION } from "./semble-downloader" import { ISembleProvider, SembleConfig, SembleContentType, SembleSearchResult, SEMBLE_DEFAULTS } from "./types" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" @@ -121,9 +121,11 @@ export class SembleProvider implements ISembleProvider { console.log("[SembleProvider] Semble found and ready.") - // Semble indexes on-the-fly, so we mark as "Indexed" (ready for search) + // Semble indexes on-the-fly, so we mark as "Indexed" (ready for search). + // The version is included in the status message so the UI (CodeIndexPopover) + // surfaces which semble release is active. this._state = "Indexed" - this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready")) + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION })) this._isInitialized = true } @@ -144,7 +146,7 @@ export class SembleProvider implements ISembleProvider { // Semble indexes on-the-fly — no separate indexing step needed. // Mark as indexed/ready. this._state = "Indexed" - this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready")) + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION })) } /** diff --git a/src/services/code-index/semble/semble-downloader.ts b/src/services/code-index/semble/semble-downloader.ts index 3245b82db9..4ee1b72406 100644 --- a/src/services/code-index/semble/semble-downloader.ts +++ b/src/services/code-index/semble/semble-downloader.ts @@ -20,7 +20,11 @@ const SEMBLE_ARCHIVES: Record = { "win32-x64": { archive: "semble-windows-x64-fast.zip", binary: "semble.exe" }, } -const SEMBLE_VERSION = "v0.4.1" +/** + * The bundled semble version. Surfaced to the UI via the provider's + * system-state message so users can see which version is active. + */ +export const SEMBLE_VERSION = "v0.4.1" const DOWNLOAD_BASE_URL = `https://github.com/Zoo-Code-Org/sembleexec/releases/download/${SEMBLE_VERSION}` const VERSION_FILE = ".semble-version"