Merge branch 'main' into catrielmuller/kilo-opencode-v1.2.21

This commit is contained in:
Catriel Müller
2026-03-17 17:52:56 -03:00
committed by GitHub
215 changed files with 4151 additions and 1553 deletions
+25 -25
View File
@@ -27,7 +27,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -79,7 +79,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -112,7 +112,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -142,7 +142,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -171,7 +171,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@kilocode/plugin": "workspace:*",
@@ -203,7 +203,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -213,7 +213,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -233,7 +233,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/util": "workspace:*",
@@ -263,7 +263,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -315,7 +315,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.0.48",
"version": "7.0.49",
"bin": {
"opencode": "./bin/opencode",
},
@@ -435,7 +435,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"zod": "catalog:",
@@ -449,14 +449,14 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -467,7 +467,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -490,7 +490,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -537,7 +537,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"zod": "catalog:",
},
@@ -581,7 +581,7 @@
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"@typescript/native-preview": "7.0.0-dev.20260316.1",
"ai": "5.0.124",
"diff": "8.0.2",
"dompurify": "3.3.1",
@@ -2062,21 +2062,21 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260316.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260316.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-s+QGNx+3zxTZBuZw3oNOFlHqpbmg0cTgBd/b6SRZ5mo3vFChkhflYqRW2IvTvU9a3PPX3bQAkQ/gWbDZCmNC3Q=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260316.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TjeMEMabLsc5VNYy8WVlu1oHBVqibwSbkIRSyqANFxyD6iWnCFquDvliwErVo8TFIu0c8C+C+tgFSvYkhVZMMw=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3bkD9QuIjxETtp6J1l5X2oKgudJ8z+8fwUq0izCjK1JrIs2vW1aQnbzxhynErSyHWH7URGhHHzcsXHbikckAsg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260316.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Lv/JmtMfNbMJiIEZlByQ5zSR1t9WoE8rFuZxU0vpiyfUEjSbuBMG8pt+Ryqj6uiylR3XThlV3EaVYsJ7Um6n8w=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OjrZBq8XJkB7uCQvT1AZ1FPsp+lT0cHxY5SisE+ZTAU6V0IHAZMwJ7J/mnwlGsBcCKRLBT+lX3hgEuOTSwHr9w=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vItkqjOuVY9OfqdovSyEjnAbNMM+QGM9AqzGRknX1nZjGlWXsUTL3IPuv5by69SOqw5TLi8ddx82cyu6F3ZRVQ=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qhp06OObkwy5B+PlAhAmq+Ls3GVt4LHAovrTRcpLB3Mk3yJ0h9DnIQwPQiayp16TdvTsGHI3jdIX4MGm5L/ghA=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xA4DekkAesjnWyp8p0iF79Rf0q2NVszxedd9M2Ztb0WBSDQFiECVYJSQMFd4+FKNiSq9DnadPy68Dly+B1r17A=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fPRw0zfTBeVmrkgi5Le+sSwoeAz6pIdvcsa1OYZcrspueS9hn3qSC5bLEc5yX4NJP1vItadBqyGLUQ7u8FJjow=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "x64" }, "sha512-osY+4HCIpi9Bu4jNz49k8BVOB9A04BG6mWF7WltmAQWBIAeosa4n/qtKokfAZDTD5/moHSn20p7hZAlGI8JWjw=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-KxY1i+HxeSFfzZ+HVsKwMGBM79laTRZv1ibFqHu22CEsfSPDt4yiV1QFis8Nw7OBXswNqJG/UGqY47VP8FeTvw=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-DcWceiTXClIakJhk0+8KjQ+pBp435HaA6uw9EtDTo75uWUEPVf9D489KKbylRChci/paYX8uPKlROo9+6N8M9g=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LvpV1hyQS0U9yMLHgWexhC7oSeBpcNbIJtYC6Iyvu63Mb6J/cP0k2fQmnAVB2yesMMQFtuY6v2YIx17vE0Ymfw=="],
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
+2 -2
View File
@@ -50,7 +50,7 @@
"marked-shiki": "1.2.1",
"@playwright/test": "1.51.0",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"@typescript/native-preview": "7.0.0-dev.20260316.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"shiki": "3.20.0",
@@ -111,6 +111,6 @@
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
},
"version": "7.0.48",
"version": "7.0.49",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "7.0.48",
"version": "7.0.49",
"description": "",
"type": "module",
"exports": {
+1
View File
@@ -17,6 +17,7 @@
"emitDeclarationOnly": true,
"outDir": "node_modules/.ts-dist",
"isolatedModules": true,
"types": ["bun", "node"],
"paths": {
"@/*": ["./src/*"]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "1.2.21"
version = "7.0.49"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.21/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.21/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.21/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.21/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.21/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.0.48",
"version": "7.0.49",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "المهارات",
"marketplace.tab.mcpServers": "خوادم MCP",
"marketplace.tab.modes": "الأوضاع",
"marketplace.search.skills": "البحث في المهارات...",
"marketplace.category.all": "الكل",
"marketplace.empty.skills": "لم يتم العثور على مهارات",
"marketplace.placeholder": "سيتم تنفيذه لاحقاً",
"marketplace.card.installed": "مثبت",
"marketplace.card.install": "تثبيت",
"marketplace.card.remove": "إزالة",
"marketplace.card.removeScope": "إزالة ({{scope}})",
"marketplace.card.showMore": "عرض المزيد",
"marketplace.card.showLess": "عرض أقل",
"marketplace.install.title": "تثبيت {{name}}",
"marketplace.install.scope": "النطاق",
"marketplace.install.scope.project": "المشروع",
@@ -43,4 +43,25 @@ export const dict = {
"هل أنت متأكد أنك تريد إزالة هذا الـ {{type}}؟ سيؤدي هذا إلى إزالته من تكوين {{scope}} الخاص بك.",
"marketplace.remove.cancel": "إلغاء",
"marketplace.remove.confirm.button": "إزالة",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "بحث...",
"marketplace.filter.all": "جميع العناصر",
"marketplace.filter.notInstalled": "غير مثبت",
"marketplace.empty": "لم يتم العثور على عناصر",
"marketplace.badge.mcpServer": "خادم MCP",
"marketplace.badge.mode": "وضع",
"marketplace.card.by": "بواسطة {{author}}",
"marketplace.install.method": "طريقة التثبيت",
"marketplace.install.parameters": "المعلمات",
"marketplace.install.optional": "(اختياري)",
"marketplace.install.required": "{{name}} مطلوب",
"marketplace.scope.project": "مشروع",
"marketplace.scope.global": "عام",
"marketplace.remove.type.mcp": "خادم MCP",
"marketplace.remove.type.skill": "مهارة",
"marketplace.remove.type.mode": "وضع",
"marketplace.remove.failed": "فشلت إزالة {{name}}",
"marketplace.install": "تثبيت",
"marketplace.filter.installed": "مثبت",
"marketplace.error.dismiss": "تجاهل",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "Servidores MCP",
"marketplace.tab.modes": "Modos",
"marketplace.search.skills": "Buscar skills...",
"marketplace.category.all": "Todos",
"marketplace.empty.skills": "Nenhuma skill encontrada",
"marketplace.placeholder": "A ser implementado",
"marketplace.card.installed": "Instalado",
"marketplace.card.install": "Instalar",
"marketplace.card.remove": "Remover",
"marketplace.card.removeScope": "Remover ({{scope}})",
"marketplace.card.showMore": "Mostrar mais",
"marketplace.card.showLess": "Mostrar menos",
"marketplace.install.title": "Instalar {{name}}",
"marketplace.install.scope": "Escopo",
"marketplace.install.scope.project": "Projeto",
@@ -43,4 +43,25 @@ export const dict = {
"Tem certeza que deseja remover este {{type}}? Isso o removerá da sua configuração {{scope}}.",
"marketplace.remove.cancel": "Cancelar",
"marketplace.remove.confirm.button": "Remover",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Pesquisar...",
"marketplace.filter.all": "Todos os Itens",
"marketplace.filter.notInstalled": "Não Instalado",
"marketplace.empty": "Nenhum item encontrado",
"marketplace.badge.mcpServer": "Servidor MCP",
"marketplace.badge.mode": "Modo",
"marketplace.card.by": "por {{author}}",
"marketplace.install.method": "Método de Instalação",
"marketplace.install.parameters": "Parâmetros",
"marketplace.install.optional": "(opcional)",
"marketplace.install.required": "{{name}} é obrigatório",
"marketplace.scope.project": "projeto",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "servidor MCP",
"marketplace.remove.type.skill": "habilidade",
"marketplace.remove.type.mode": "modo",
"marketplace.remove.failed": "Falha ao remover {{name}}",
"marketplace.install": "Instalar",
"marketplace.filter.installed": "Instalado",
"marketplace.error.dismiss": "Dispensar",
}
+23 -2
View File
@@ -24,14 +24,14 @@ export const dict = {
"marketplace.tab.skills": "Vještine",
"marketplace.tab.mcpServers": "MCP Serveri",
"marketplace.tab.modes": "Modovi",
"marketplace.search.skills": "Pretraži vještine...",
"marketplace.category.all": "Sve",
"marketplace.empty.skills": "Nema pronađenih vještina",
"marketplace.placeholder": "Biće implementirano",
"marketplace.card.installed": "Instalirano",
"marketplace.card.install": "Instaliraj",
"marketplace.card.remove": "Ukloni",
"marketplace.card.removeScope": "Ukloni ({{scope}})",
"marketplace.card.showMore": "Prikaži više",
"marketplace.card.showLess": "Prikaži manje",
"marketplace.install.title": "Instaliraj {{name}}",
"marketplace.install.scope": "Opseg",
"marketplace.install.scope.project": "Projekat",
@@ -48,4 +48,25 @@ export const dict = {
"Jeste li sigurni da želite ukloniti ovaj {{type}}? Ovo će ga ukloniti iz vaše {{scope}} konfiguracije.",
"marketplace.remove.cancel": "Otkaži",
"marketplace.remove.confirm.button": "Ukloni",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Pretraži...",
"marketplace.filter.all": "Sve stavke",
"marketplace.filter.notInstalled": "Nije instalirano",
"marketplace.empty": "Nema pronađenih stavki",
"marketplace.badge.mcpServer": "MCP Server",
"marketplace.badge.mode": "Režim",
"marketplace.card.by": "od {{author}}",
"marketplace.install.method": "Metoda instalacije",
"marketplace.install.parameters": "Parametri",
"marketplace.install.optional": "(opciono)",
"marketplace.install.required": "{{name}} je obavezno",
"marketplace.scope.project": "projekat",
"marketplace.scope.global": "globalno",
"marketplace.remove.type.mcp": "MCP server",
"marketplace.remove.type.skill": "vještina",
"marketplace.remove.type.mode": "režim",
"marketplace.remove.failed": "Uklanjanje {{name}} nije uspjelo",
"marketplace.install": "Instaliraj",
"marketplace.filter.installed": "Instalirano",
"marketplace.error.dismiss": "Odbaci",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "MCP-servere",
"marketplace.tab.modes": "Tilstande",
"marketplace.search.skills": "Søg efter skills...",
"marketplace.category.all": "Alle",
"marketplace.empty.skills": "Ingen skills fundet",
"marketplace.placeholder": "Skal implementeres",
"marketplace.card.installed": "Installeret",
"marketplace.card.install": "Installer",
"marketplace.card.remove": "Fjern",
"marketplace.card.removeScope": "Fjern ({{scope}})",
"marketplace.card.showMore": "Vis mere",
"marketplace.card.showLess": "Vis mindre",
"marketplace.install.title": "Installer {{name}}",
"marketplace.install.scope": "Omfang",
"marketplace.install.scope.project": "Projekt",
@@ -43,4 +43,25 @@ export const dict = {
"Er du sikker på, at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}} konfiguration.",
"marketplace.remove.cancel": "Annuller",
"marketplace.remove.confirm.button": "Fjern",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Søg...",
"marketplace.filter.all": "Alle elementer",
"marketplace.filter.notInstalled": "Ikke installeret",
"marketplace.empty": "Ingen elementer fundet",
"marketplace.badge.mcpServer": "MCP-server",
"marketplace.badge.mode": "Tilstand",
"marketplace.card.by": "af {{author}}",
"marketplace.install.method": "Installationsmetode",
"marketplace.install.parameters": "Parametre",
"marketplace.install.optional": "(valgfrit)",
"marketplace.install.required": "{{name}} er påkrævet",
"marketplace.scope.project": "projekt",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP-server",
"marketplace.remove.type.skill": "færdighed",
"marketplace.remove.type.mode": "tilstand",
"marketplace.remove.failed": "Kunne ikke fjerne {{name}}",
"marketplace.install": "Installer",
"marketplace.filter.installed": "Installeret",
"marketplace.error.dismiss": "Afvis",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "MCP-Server",
"marketplace.tab.modes": "Modi",
"marketplace.search.skills": "Skills suchen...",
"marketplace.category.all": "Alle",
"marketplace.empty.skills": "Keine Skills gefunden",
"marketplace.placeholder": "Noch nicht implementiert",
"marketplace.card.installed": "Installiert",
"marketplace.card.install": "Installieren",
"marketplace.card.remove": "Entfernen",
"marketplace.card.removeScope": "Entfernen ({{scope}})",
"marketplace.card.showMore": "Mehr anzeigen",
"marketplace.card.showLess": "Weniger anzeigen",
"marketplace.install.title": "{{name}} installieren",
"marketplace.install.scope": "Bereich",
"marketplace.install.scope.project": "Projekt",
@@ -43,4 +43,25 @@ export const dict = {
"Sind Sie sicher, dass Sie diese {{type}} entfernen möchten? Dadurch wird sie aus Ihrer {{scope}}-Konfiguration entfernt.",
"marketplace.remove.cancel": "Abbrechen",
"marketplace.remove.confirm.button": "Entfernen",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Suchen...",
"marketplace.filter.all": "Alle Elemente",
"marketplace.filter.notInstalled": "Nicht installiert",
"marketplace.empty": "Keine Elemente gefunden",
"marketplace.badge.mcpServer": "MCP-Server",
"marketplace.badge.mode": "Modus",
"marketplace.card.by": "von {{author}}",
"marketplace.install.method": "Installationsmethode",
"marketplace.install.parameters": "Parameter",
"marketplace.install.optional": "(optional)",
"marketplace.install.required": "{{name}} ist erforderlich",
"marketplace.scope.project": "Projekt",
"marketplace.scope.global": "Global",
"marketplace.remove.type.mcp": "MCP-Server",
"marketplace.remove.type.skill": "Skill",
"marketplace.remove.type.mode": "Modus",
"marketplace.remove.failed": "Fehler beim Entfernen von {{name}}",
"marketplace.install": "Installieren",
"marketplace.filter.installed": "Installiert",
"marketplace.error.dismiss": "Verwerfen",
}
+23 -2
View File
@@ -21,14 +21,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "MCP Servers",
"marketplace.tab.modes": "Modes",
"marketplace.search.skills": "Search skills...",
"marketplace.category.all": "All",
"marketplace.empty.skills": "No skills found",
"marketplace.placeholder": "To be implemented",
"marketplace.card.installed": "Installed",
"marketplace.card.install": "Install",
"marketplace.card.remove": "Remove",
"marketplace.card.removeScope": "Remove ({{scope}})",
"marketplace.card.showMore": "Show more",
"marketplace.card.showLess": "Show less",
"marketplace.install.title": "Install {{name}}",
"marketplace.install.scope": "Scope",
"marketplace.install.scope.project": "Project",
@@ -45,4 +45,25 @@ export const dict = {
"Are you sure you want to remove this {{type}}? This will remove it from your {{scope}} configuration.",
"marketplace.remove.cancel": "Cancel",
"marketplace.remove.confirm.button": "Remove",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Search...",
"marketplace.filter.all": "All Items",
"marketplace.filter.notInstalled": "Not Installed",
"marketplace.empty": "No items found",
"marketplace.badge.mcpServer": "MCP Server",
"marketplace.badge.mode": "Mode",
"marketplace.card.by": "by {{author}}",
"marketplace.install.method": "Installation Method",
"marketplace.install.parameters": "Parameters",
"marketplace.install.optional": "(optional)",
"marketplace.install.required": "{{name}} is required",
"marketplace.scope.project": "project",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP server",
"marketplace.remove.type.skill": "skill",
"marketplace.remove.type.mode": "mode",
"marketplace.remove.failed": "Failed to remove {{name}}",
"marketplace.install": "Install",
"marketplace.filter.installed": "Installed",
"marketplace.error.dismiss": "Dismiss",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "Servidores MCP",
"marketplace.tab.modes": "Modos",
"marketplace.search.skills": "Buscar skills...",
"marketplace.category.all": "Todos",
"marketplace.empty.skills": "No se encontraron skills",
"marketplace.placeholder": "Por implementar",
"marketplace.card.installed": "Instalado",
"marketplace.card.install": "Instalar",
"marketplace.card.remove": "Eliminar",
"marketplace.card.removeScope": "Eliminar ({{scope}})",
"marketplace.card.showMore": "Mostrar más",
"marketplace.card.showLess": "Mostrar menos",
"marketplace.install.title": "Instalar {{name}}",
"marketplace.install.scope": "Alcance",
"marketplace.install.scope.project": "Proyecto",
@@ -43,4 +43,25 @@ export const dict = {
"¿Estás seguro de que deseas eliminar este {{type}}? Esto lo eliminará de tu configuración {{scope}}.",
"marketplace.remove.cancel": "Cancelar",
"marketplace.remove.confirm.button": "Eliminar",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Buscar...",
"marketplace.filter.all": "Todos los elementos",
"marketplace.filter.notInstalled": "No instalado",
"marketplace.empty": "No se encontraron elementos",
"marketplace.badge.mcpServer": "Servidor MCP",
"marketplace.badge.mode": "Modo",
"marketplace.card.by": "por {{author}}",
"marketplace.install.method": "Método de instalación",
"marketplace.install.parameters": "Parámetros",
"marketplace.install.optional": "(opcional)",
"marketplace.install.required": "{{name}} es obligatorio",
"marketplace.scope.project": "proyecto",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "servidor MCP",
"marketplace.remove.type.skill": "habilidad",
"marketplace.remove.type.mode": "modo",
"marketplace.remove.failed": "Error al eliminar {{name}}",
"marketplace.install": "Instalar",
"marketplace.filter.installed": "Instalado",
"marketplace.error.dismiss": "Descartar",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "Serveurs MCP",
"marketplace.tab.modes": "Modes",
"marketplace.search.skills": "Rechercher des skills...",
"marketplace.category.all": "Tout",
"marketplace.empty.skills": "Aucune skill trouvée",
"marketplace.placeholder": "À implémenter",
"marketplace.card.installed": "Installé",
"marketplace.card.install": "Installer",
"marketplace.card.remove": "Supprimer",
"marketplace.card.removeScope": "Supprimer ({{scope}})",
"marketplace.card.showMore": "Afficher plus",
"marketplace.card.showLess": "Afficher moins",
"marketplace.install.title": "Installer {{name}}",
"marketplace.install.scope": "Portée",
"marketplace.install.scope.project": "Projet",
@@ -43,4 +43,25 @@ export const dict = {
"Êtes-vous sûr de vouloir supprimer ce {{type}} ? Cela le supprimera de votre configuration {{scope}}.",
"marketplace.remove.cancel": "Annuler",
"marketplace.remove.confirm.button": "Supprimer",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Rechercher...",
"marketplace.filter.all": "Tous les éléments",
"marketplace.filter.notInstalled": "Non installé",
"marketplace.empty": "Aucun élément trouvé",
"marketplace.badge.mcpServer": "Serveur MCP",
"marketplace.badge.mode": "Mode",
"marketplace.card.by": "par {{author}}",
"marketplace.install.method": "Méthode d'installation",
"marketplace.install.parameters": "Paramètres",
"marketplace.install.optional": "(facultatif)",
"marketplace.install.required": "{{name}} est requis",
"marketplace.scope.project": "projet",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "serveur MCP",
"marketplace.remove.type.skill": "compétence",
"marketplace.remove.type.mode": "mode",
"marketplace.remove.failed": "Échec de la suppression de {{name}}",
"marketplace.install": "Installer",
"marketplace.filter.installed": "Installé",
"marketplace.error.dismiss": "Ignorer",
}
+23 -2
View File
@@ -18,14 +18,14 @@ export const dict = {
"marketplace.tab.skills": "スキル",
"marketplace.tab.mcpServers": "MCPサーバー",
"marketplace.tab.modes": "モード",
"marketplace.search.skills": "スキルを検索...",
"marketplace.category.all": "すべて",
"marketplace.empty.skills": "スキルが見つかりません",
"marketplace.placeholder": "未実装",
"marketplace.card.installed": "インストール済み",
"marketplace.card.install": "インストール",
"marketplace.card.remove": "削除",
"marketplace.card.removeScope": "削除 ({{scope}})",
"marketplace.card.showMore": "もっと見る",
"marketplace.card.showLess": "折りたたむ",
"marketplace.install.title": "{{name}} のインストール",
"marketplace.install.scope": "スコープ",
"marketplace.install.scope.project": "プロジェクト",
@@ -41,4 +41,25 @@ export const dict = {
"marketplace.remove.confirm": "この{{type}}を削除してもよろしいですか? これにより、{{scope}}設定から削除されます。",
"marketplace.remove.cancel": "キャンセル",
"marketplace.remove.confirm.button": "削除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "検索...",
"marketplace.filter.all": "すべてのアイテム",
"marketplace.filter.notInstalled": "未インストール",
"marketplace.empty": "アイテムが見つかりません",
"marketplace.badge.mcpServer": "MCPサーバー",
"marketplace.badge.mode": "モード",
"marketplace.card.by": "作成者: {{author}}",
"marketplace.install.method": "インストール方法",
"marketplace.install.parameters": "パラメーター",
"marketplace.install.optional": "(任意)",
"marketplace.install.required": "{{name}} は必須です",
"marketplace.scope.project": "プロジェクト",
"marketplace.scope.global": "グローバル",
"marketplace.remove.type.mcp": "MCPサーバー",
"marketplace.remove.type.skill": "スキル",
"marketplace.remove.type.mode": "モード",
"marketplace.remove.failed": "{{name}} の削除に失敗しました",
"marketplace.install": "インストール",
"marketplace.filter.installed": "インストール済み",
"marketplace.error.dismiss": "閉じる",
}
+23 -2
View File
@@ -18,14 +18,14 @@ export const dict = {
"marketplace.tab.skills": "스킬",
"marketplace.tab.mcpServers": "MCP 서버",
"marketplace.tab.modes": "모드",
"marketplace.search.skills": "스킬 검색...",
"marketplace.category.all": "전체",
"marketplace.empty.skills": "스킬을 찾을 수 없습니다",
"marketplace.placeholder": "구현 예정",
"marketplace.card.installed": "설치됨",
"marketplace.card.install": "설치",
"marketplace.card.remove": "제거",
"marketplace.card.removeScope": "제거 ({{scope}})",
"marketplace.card.showMore": "더 보기",
"marketplace.card.showLess": "접기",
"marketplace.install.title": "{{name}} 설치",
"marketplace.install.scope": "범위",
"marketplace.install.scope.project": "프로젝트",
@@ -41,4 +41,25 @@ export const dict = {
"marketplace.remove.confirm": "이 {{type}}을(를) 제거하시겠습니까? {{scope}} 구성에서 제거됩니다.",
"marketplace.remove.cancel": "취소",
"marketplace.remove.confirm.button": "제거",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "검색...",
"marketplace.filter.all": "모든 항목",
"marketplace.filter.notInstalled": "설치되지 않음",
"marketplace.empty": "항목을 찾을 수 없음",
"marketplace.badge.mcpServer": "MCP 서버",
"marketplace.badge.mode": "모드",
"marketplace.card.by": "제작: {{author}}",
"marketplace.install.method": "설치 방법",
"marketplace.install.parameters": "매개변수",
"marketplace.install.optional": "(선택 사항)",
"marketplace.install.required": "{{name}}이(가) 필요합니다",
"marketplace.scope.project": "프로젝트",
"marketplace.scope.global": "글로벌",
"marketplace.remove.type.mcp": "MCP 서버",
"marketplace.remove.type.skill": "스킬",
"marketplace.remove.type.mode": "모드",
"marketplace.remove.failed": "{{name}} 제거 실패",
"marketplace.install": "설치",
"marketplace.filter.installed": "설치됨",
"marketplace.error.dismiss": "닫기",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "MCP-servere",
"marketplace.tab.modes": "Moduser",
"marketplace.search.skills": "Søk etter skills...",
"marketplace.category.all": "Alle",
"marketplace.empty.skills": "Ingen skills funnet",
"marketplace.placeholder": "Skal implementeres",
"marketplace.card.installed": "Installert",
"marketplace.card.install": "Installer",
"marketplace.card.remove": "Fjern",
"marketplace.card.removeScope": "Fjern ({{scope}})",
"marketplace.card.showMore": "Vis mer",
"marketplace.card.showLess": "Vis mindre",
"marketplace.install.title": "Installer {{name}}",
"marketplace.install.scope": "Omfang",
"marketplace.install.scope.project": "Prosjekt",
@@ -43,4 +43,25 @@ export const dict = {
"Er du sikker på at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}}-konfigurasjon.",
"marketplace.remove.cancel": "Avbryt",
"marketplace.remove.confirm.button": "Fjern",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Søk...",
"marketplace.filter.all": "Alle elementer",
"marketplace.filter.notInstalled": "Ikke installert",
"marketplace.empty": "Ingen elementer funnet",
"marketplace.badge.mcpServer": "MCP-server",
"marketplace.badge.mode": "Modus",
"marketplace.card.by": "av {{author}}",
"marketplace.install.method": "Installasjonsmetode",
"marketplace.install.parameters": "Parametere",
"marketplace.install.optional": "(valgfritt)",
"marketplace.install.required": "{{name}} er påkrevd",
"marketplace.scope.project": "prosjekt",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP-server",
"marketplace.remove.type.skill": "ferdighet",
"marketplace.remove.type.mode": "modus",
"marketplace.remove.failed": "Kunne ikke fjerne {{name}}",
"marketplace.install": "Installer",
"marketplace.filter.installed": "Installert",
"marketplace.error.dismiss": "Avvis",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Skills",
"marketplace.tab.mcpServers": "Serwery MCP",
"marketplace.tab.modes": "Tryby",
"marketplace.search.skills": "Szukaj skills...",
"marketplace.category.all": "Wszystkie",
"marketplace.empty.skills": "Nie znaleziono skills",
"marketplace.placeholder": "Do zaimplementowania",
"marketplace.card.installed": "Zainstalowano",
"marketplace.card.install": "Zainstaluj",
"marketplace.card.remove": "Usuń",
"marketplace.card.removeScope": "Usuń ({{scope}})",
"marketplace.card.showMore": "Pokaż więcej",
"marketplace.card.showLess": "Pokaż mniej",
"marketplace.install.title": "Instalacja: {{name}}",
"marketplace.install.scope": "Zakres",
"marketplace.install.scope.project": "Projekt",
@@ -43,4 +43,25 @@ export const dict = {
"Czy na pewno chcesz usunąć ten {{type}}? Spowoduje to usunięcie z konfiguracji: {{scope}}.",
"marketplace.remove.cancel": "Anuluj",
"marketplace.remove.confirm.button": "Usuń",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Szukaj...",
"marketplace.filter.all": "Wszystkie elementy",
"marketplace.filter.notInstalled": "Nie zainstalowano",
"marketplace.empty": "Nie znaleziono elementów",
"marketplace.badge.mcpServer": "Serwer MCP",
"marketplace.badge.mode": "Tryb",
"marketplace.card.by": "autor: {{author}}",
"marketplace.install.method": "Metoda instalacji",
"marketplace.install.parameters": "Parametry",
"marketplace.install.optional": "(opcjonalne)",
"marketplace.install.required": "{{name}} jest wymagane",
"marketplace.scope.project": "projekt",
"marketplace.scope.global": "globalny",
"marketplace.remove.type.mcp": "serwer MCP",
"marketplace.remove.type.skill": "umiejętność",
"marketplace.remove.type.mode": "tryb",
"marketplace.remove.failed": "Nie udało się usunąć {{name}}",
"marketplace.install": "Zainstaluj",
"marketplace.filter.installed": "Zainstalowano",
"marketplace.error.dismiss": "Odrzuć",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "Навыки",
"marketplace.tab.mcpServers": "MCP-серверы",
"marketplace.tab.modes": "Режимы",
"marketplace.search.skills": "Поиск навыков...",
"marketplace.category.all": "Все",
"marketplace.empty.skills": "Навыки не найдены",
"marketplace.placeholder": "Будет реализовано",
"marketplace.card.installed": "Установлено",
"marketplace.card.install": "Установить",
"marketplace.card.remove": "Удалить",
"marketplace.card.removeScope": "Удалить ({{scope}})",
"marketplace.card.showMore": "Показать больше",
"marketplace.card.showLess": "Показать меньше",
"marketplace.install.title": "Установка {{name}}",
"marketplace.install.scope": "Область",
"marketplace.install.scope.project": "Проект",
@@ -43,4 +43,25 @@ export const dict = {
"Вы уверены, что хотите удалить этот {{type}}? Это удалит его из вашей конфигурации ({{scope}}).",
"marketplace.remove.cancel": "Отмена",
"marketplace.remove.confirm.button": "Удалить",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Поиск...",
"marketplace.filter.all": "Все элементы",
"marketplace.filter.notInstalled": "Не установлено",
"marketplace.empty": "Элементы не найдены",
"marketplace.badge.mcpServer": "MCP-сервер",
"marketplace.badge.mode": "Режим",
"marketplace.card.by": "от {{author}}",
"marketplace.install.method": "Способ установки",
"marketplace.install.parameters": "Параметры",
"marketplace.install.optional": "(необязательно)",
"marketplace.install.required": "Требуется {{name}}",
"marketplace.scope.project": "проект",
"marketplace.scope.global": "глобально",
"marketplace.remove.type.mcp": "MCP-сервер",
"marketplace.remove.type.skill": "навык",
"marketplace.remove.type.mode": "режим",
"marketplace.remove.failed": "Не удалось удалить {{name}}",
"marketplace.install": "Установить",
"marketplace.filter.installed": "Установлено",
"marketplace.error.dismiss": "Закрыть",
}
+23 -2
View File
@@ -19,14 +19,14 @@ export const dict = {
"marketplace.tab.skills": "ทักษะ",
"marketplace.tab.mcpServers": "เซิร์ฟเวอร์ MCP",
"marketplace.tab.modes": "โหมด",
"marketplace.search.skills": "ค้นหาทักษะ...",
"marketplace.category.all": "ทั้งหมด",
"marketplace.empty.skills": "ไม่พบทักษะ",
"marketplace.placeholder": "ยังไม่ได้ดำเนินการ",
"marketplace.card.installed": "ติดตั้งแล้ว",
"marketplace.card.install": "ติดตั้ง",
"marketplace.card.remove": "ลบ",
"marketplace.card.removeScope": "ลบ ({{scope}})",
"marketplace.card.showMore": "แสดงเพิ่มเติม",
"marketplace.card.showLess": "แสดงน้อยลง",
"marketplace.install.title": "ติดตั้ง {{name}}",
"marketplace.install.scope": "ขอบเขต",
"marketplace.install.scope.project": "โปรเจกต์",
@@ -43,4 +43,25 @@ export const dict = {
"คุณแน่ใจหรือไม่ว่าต้องการลบ {{type}} นี้? สิ่งนี้จะลบออกจากการกำหนดค่า {{scope}} ของคุณ",
"marketplace.remove.cancel": "ยกเลิก",
"marketplace.remove.confirm.button": "ลบ",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "ค้นหา...",
"marketplace.filter.all": "รายการทั้งหมด",
"marketplace.filter.notInstalled": "ยังไม่ได้ติดตั้ง",
"marketplace.empty": "ไม่พบรายการ",
"marketplace.badge.mcpServer": "เซิร์ฟเวอร์ MCP",
"marketplace.badge.mode": "โหมด",
"marketplace.card.by": "โดย {{author}}",
"marketplace.install.method": "วิธีการติดตั้ง",
"marketplace.install.parameters": "พารามิเตอร์",
"marketplace.install.optional": "(ไม่บังคับ)",
"marketplace.install.required": "จำเป็นต้องใช้ {{name}}",
"marketplace.scope.project": "โปรเจ็กต์",
"marketplace.scope.global": "โกลบอล",
"marketplace.remove.type.mcp": "เซิร์ฟเวอร์ MCP",
"marketplace.remove.type.skill": "ทักษะ",
"marketplace.remove.type.mode": "โหมด",
"marketplace.remove.failed": "ไม่สามารถลบ {{name}} ได้",
"marketplace.install": "ติดตั้ง",
"marketplace.filter.installed": "ติดตั้งแล้ว",
"marketplace.error.dismiss": "ปิด",
}
+50
View File
@@ -14,4 +14,54 @@ export const dict = {
// Reasoning block label
"ui.reasoning.label": "Akıl Yürütme",
// Marketplace
"marketplace.tab.skills": "Yetenekler",
"marketplace.tab.mcpServers": "MCP Sunucuları",
"marketplace.tab.modes": "Modlar",
"marketplace.category.all": "Tümü",
"marketplace.placeholder": "Uygulanacak",
"marketplace.card.installed": "Yüklendi",
"marketplace.card.install": "Yükle",
"marketplace.card.remove": "Kaldır",
"marketplace.card.removeScope": "Kaldır ({{scope}})",
"marketplace.card.showMore": "Daha fazla göster",
"marketplace.card.showLess": "Daha az göster",
"marketplace.install.title": "{{name}} yükle",
"marketplace.install.scope": "Kapsam",
"marketplace.install.scope.project": "Proje",
"marketplace.install.scope.global": "Genel",
"marketplace.install.prerequisites": "Ön koşullar",
"marketplace.install.installing": "Yükleniyor...",
"marketplace.install.cancel": "İptal",
"marketplace.install.success": "Başarıyla yüklendi!",
"marketplace.install.failed": "Yükleme başarısız oldu",
"marketplace.install.done": "Bitti",
"marketplace.install.close": "Kapat",
"marketplace.remove.title": "{{name}} kaldırılsın mı?",
"marketplace.remove.confirm":
"Bu {{type}} öğesini kaldırmak istediğinizden emin misiniz? Bu, {{scope}} yapılandırmanızdan kaldırılacaktır.",
"marketplace.remove.cancel": "İptal",
"marketplace.remove.confirm.button": "Kaldır",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Ara...",
"marketplace.filter.all": "Tüm Öğeler",
"marketplace.filter.notInstalled": "Yüklü Değil",
"marketplace.empty": "Öğe bulunamadı",
"marketplace.badge.mcpServer": "MCP Sunucusu",
"marketplace.badge.mode": "Mod",
"marketplace.card.by": "{{author}} tarafından",
"marketplace.install.method": "Yükleme Yöntemi",
"marketplace.install.parameters": "Parametreler",
"marketplace.install.optional": "(isteğe bağlı)",
"marketplace.install.required": "{{name}} gereklidir",
"marketplace.scope.project": "proje",
"marketplace.scope.global": "genel",
"marketplace.remove.type.mcp": "MCP sunucusu",
"marketplace.remove.type.skill": "yetenek",
"marketplace.remove.type.mode": "mod",
"marketplace.remove.failed": "{{name}} kaldırılamadı",
"marketplace.install": "Yükle",
"marketplace.filter.installed": "Yüklendi",
"marketplace.error.dismiss": "Kapat",
}
+23 -2
View File
@@ -17,14 +17,14 @@ export const dict = {
"marketplace.tab.skills": "技能",
"marketplace.tab.mcpServers": "MCP 服务器",
"marketplace.tab.modes": "模式",
"marketplace.search.skills": "搜索技能...",
"marketplace.category.all": "全部",
"marketplace.empty.skills": "未找到技能",
"marketplace.placeholder": "待实现",
"marketplace.card.installed": "已安装",
"marketplace.card.install": "安装",
"marketplace.card.remove": "移除",
"marketplace.card.removeScope": "移除 ({{scope}})",
"marketplace.card.showMore": "展开",
"marketplace.card.showLess": "收起",
"marketplace.install.title": "安装 {{name}}",
"marketplace.install.scope": "作用域",
"marketplace.install.scope.project": "项目",
@@ -40,4 +40,25 @@ export const dict = {
"marketplace.remove.confirm": "确定要移除此 {{type}} 吗?这会将其从你的 {{scope}} 配置中移除。",
"marketplace.remove.cancel": "取消",
"marketplace.remove.confirm.button": "移除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "搜索...",
"marketplace.filter.all": "所有项目",
"marketplace.filter.notInstalled": "未安装",
"marketplace.empty": "未找到项目",
"marketplace.badge.mcpServer": "MCP 服务器",
"marketplace.badge.mode": "模式",
"marketplace.card.by": "作者:{{author}}",
"marketplace.install.method": "安装方法",
"marketplace.install.parameters": "参数",
"marketplace.install.optional": "(可选)",
"marketplace.install.required": "{{name}} 是必填项",
"marketplace.scope.project": "项目",
"marketplace.scope.global": "全局",
"marketplace.remove.type.mcp": "MCP 服务器",
"marketplace.remove.type.skill": "技能",
"marketplace.remove.type.mode": "模式",
"marketplace.remove.failed": "移除 {{name}} 失败",
"marketplace.install": "安装",
"marketplace.filter.installed": "已安装",
"marketplace.error.dismiss": "关闭",
}
+23 -2
View File
@@ -17,14 +17,14 @@ export const dict = {
"marketplace.tab.skills": "技能",
"marketplace.tab.mcpServers": "MCP 伺服器",
"marketplace.tab.modes": "模式",
"marketplace.search.skills": "搜尋技能...",
"marketplace.category.all": "全部",
"marketplace.empty.skills": "找不到技能",
"marketplace.placeholder": "待實作",
"marketplace.card.installed": "已安裝",
"marketplace.card.install": "安裝",
"marketplace.card.remove": "移除",
"marketplace.card.removeScope": "移除 ({{scope}})",
"marketplace.card.showMore": "展開",
"marketplace.card.showLess": "收起",
"marketplace.install.title": "安裝 {{name}}",
"marketplace.install.scope": "作用域",
"marketplace.install.scope.project": "專案",
@@ -40,4 +40,25 @@ export const dict = {
"marketplace.remove.confirm": "確定要移除此 {{type}} 嗎?這會將其從你的 {{scope}} 設定中移除。",
"marketplace.remove.cancel": "取消",
"marketplace.remove.confirm.button": "移除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "搜尋...",
"marketplace.filter.all": "所有項目",
"marketplace.filter.notInstalled": "未安裝",
"marketplace.empty": "找不到項目",
"marketplace.badge.mcpServer": "MCP 伺服器",
"marketplace.badge.mode": "模式",
"marketplace.card.by": "作者:{{author}}",
"marketplace.install.method": "安裝方法",
"marketplace.install.parameters": "參數",
"marketplace.install.optional": "(選填)",
"marketplace.install.required": "{{name}} 是必填項",
"marketplace.scope.project": "專案",
"marketplace.scope.global": "全域",
"marketplace.remove.type.mcp": "MCP 伺服器",
"marketplace.remove.type.skill": "技能",
"marketplace.remove.type.mode": "模式",
"marketplace.remove.failed": "移除 {{name}} 失敗",
"marketplace.install": "安裝",
"marketplace.filter.installed": "已安裝",
"marketplace.error.dismiss": "關閉",
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1
View File
@@ -4,6 +4,7 @@
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": ["bun"],
"skipLibCheck": true,
"noUncheckedIndexedAccess": false
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"exports": {
@@ -73,12 +73,21 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
padding: 0 8px;
height: 36px;
background-color: var(--surface-inset-base, var(--vscode-sideBar-background));
&:hover {
background-color: var(--surface-inset-base-hover, var(--vscode-list-hoverBackground));
}
}
[data-slot="collapsible-trigger"][aria-expanded="true"] {
border-bottom: 1px solid var(--border-weak-base, var(--vscode-panel-border));
}
/* Arrow always visible (no fade-on-hover) */
[data-slot="collapsible-arrow"] {
opacity: 1;
}
/* Arrow: DOWN when collapsed, UP when expanded (matches question-dock toggle) */
[data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(0deg);
@@ -5,3 +5,14 @@
cursor: pointer;
}
}
/* VS Code theme: tool-collapsible arrows point DOWN (collapsed) and UP (expanded), never right */
html[data-theme="kilo-vscode"] [data-component="collapsible"].tool-collapsible {
[data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(0deg);
}
[data-slot="collapsible-trigger"][aria-expanded="true"] [data-slot="collapsible-arrow-icon"] {
transform: translateZ(0) rotate(180deg);
}
}
@@ -139,12 +139,16 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
}
[data-slot="collapsible-trigger"] {
height: 28px;
height: 36px;
padding: 0 8px;
gap: 6px;
background: var(--surface-inset-base);
border-radius: 0;
cursor: pointer;
&:hover {
background-color: var(--surface-inset-base-hover, var(--vscode-list-hoverBackground));
}
}
[data-slot="reasoning-header"] {
@@ -1206,7 +1206,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
return (
<Show when={throttledText()}>
<div data-component="reasoning-part">
<Collapsible open={open()} onOpenChange={setOpen}>
<Collapsible open={open()} onOpenChange={setOpen} class="tool-collapsible">
<Collapsible.Trigger>
<div data-slot="reasoning-header">
<Icon name="brain" size="small" />
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf93ca79d15deab7dc52473f68305b521c62d10e968a9258e09609ad9c04fde6
size 15837
oid sha256:67cb95242834a2ac3633b70a68a8ff9a9f343938092f06926a44639ba5c01121
size 15968
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14bf991adcbc6fcbf53f4cff2275dcec255943b5545c624231aeb73529ca2d6e
size 32478
oid sha256:6d93bf6465c33895aa54bc2479315ea2fba38d06488f3d7cdf868b60caaefd9e
size 32623
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.0.48",
"version": "7.0.49",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+117 -19
View File
@@ -1,6 +1,7 @@
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "./image-preview"
import { isAbsolutePath } from "./path-utils"
import type {
KiloClient,
@@ -365,6 +366,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "abort":
await this.handleAbort(message.sessionID)
break
case "revertSession":
this.handleRevertSession(message.sessionID, message.messageID).catch((e) =>
console.error("[Kilo New] handleRevertSession failed:", e),
)
break
case "unrevertSession":
this.handleUnrevertSession(message.sessionID).catch((e) =>
console.error("[Kilo New] handleUnrevertSession failed:", e),
)
break
case "permissionResponse":
await this.handlePermissionResponse(
message.permissionId,
@@ -428,6 +439,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "openSubAgentViewer":
vscode.commands.executeCommand("kilo-code.new.openSubAgentViewer", message.sessionID, message.title)
break
case "previewImage":
this.handlePreviewImage(message.dataUrl, message.filename)
break
case "openFile":
if (message.filePath) {
this.handleOpenFile(message.filePath, message.line, message.column)
@@ -657,7 +671,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.getMarketplace().install(message.mpItem, message.mpInstallOptions, workspace)
if (result.success) await this.disposeCliInstance(scope)
if (result.success) {
await this.disposeCliInstance(scope)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
}
this.postMessage({
type: "marketplaceInstallResult",
success: result.success,
@@ -670,7 +688,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.getMarketplace().remove(message.mpItem, scope, workspace)
if (result.success) await this.disposeCliInstance(scope)
if (result.success) {
await this.disposeCliInstance(scope)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
}
this.postMessage({
type: "marketplaceRemoveResult",
success: result.success,
@@ -1236,22 +1258,34 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
*/
private async handleRemoveMode(name: string): Promise<void> {
if (!this.client) return
let removed = false
// 1. Try CLI removal (handles .md files and legacy .kilocodemodes)
try {
const dir = this.getWorkspaceDirectory()
const result = await this.client.kilocode.removeAgent({ name, directory: dir })
if (result.error) {
console.error("[Kilo New] KiloProvider: removeAgent returned error:", result.error)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
return
}
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to remove mode:", error)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
return
if (!result.error) removed = true
} catch {
// CLI removal failed — agent may be in kilo.json instead
}
// Invalidate cache so next requestAgents fetches fresh data
// 2. Try removing from kilo.json (handles marketplace-installed modes)
if (!removed) {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const stub = { id: name, type: "mode" as const, name, description: "", content: "" }
const project = await mp.remove(stub, "project", workspace)
const global = await mp.remove(stub, "global", workspace)
if (project.success || global.success) {
await this.disposeCliInstance("global")
removed = true
}
}
if (!removed) {
console.error("[Kilo New] KiloProvider: Failed to remove mode:", name)
}
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
}
@@ -1267,12 +1301,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.client.global.dispose().catch((e: unknown) => {
console.warn("[Kilo New] global.dispose() after marketplace change failed:", e)
})
} else {
const dir = this.getWorkspaceDirectory()
await this.client.instance.dispose({ directory: dir }).catch((e: unknown) => {
console.warn("[Kilo New] instance.dispose() after marketplace change failed:", e)
})
}
// Always dispose the per-project instance so it rebuilds state from
// the (possibly updated) global + project config on the next request.
const dir = this.getWorkspaceDirectory()
await this.client.instance.dispose({ directory: dir }).catch((e: unknown) => {
console.warn("[Kilo New] instance.dispose() after marketplace change failed:", e)
})
}
/**
@@ -1697,6 +1732,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private async handleRevertSession(sessionID: string, messageID: string): Promise<void> {
if (!this.client) return
const dir = this.getWorkspaceDirectory(sessionID)
const { data, error } = await this.client.session.revert({ sessionID, messageID, directory: dir })
if (error) {
console.error("[Kilo New] KiloProvider: Failed to revert session:", error)
this.postMessage({ type: "error", message: "Failed to revert session", sessionID })
return
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
private async handleUnrevertSession(sessionID: string): Promise<void> {
if (!this.client) return
const dir = this.getWorkspaceDirectory(sessionID)
const { data, error } = await this.client.session.unrevert({ sessionID, directory: dir })
if (error) {
console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error)
this.postMessage({ type: "error", message: "Failed to redo session", sessionID })
return
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
/**
* Handle compact (context summarization) request from the webview.
*/
@@ -1809,6 +1868,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
sessionID: perm.sessionID,
toolName: perm.permission,
patterns: perm.patterns,
always: perm.always,
args: perm.metadata,
message: `Permission required: ${perm.permission}`,
tool: perm.tool,
@@ -1982,6 +2042,44 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private handlePreviewImage(dataUrl: string, filename: string): void {
const dir = this.extensionContext?.globalStorageUri
if (!dir) return
const img = parseImage(dataUrl, filename)
if (!img) return
const root = vscode.Uri.joinPath(dir, getPreviewDir())
const uri = vscode.Uri.joinPath(dir, buildPreviewPath(img.name, Date.now()))
const clean = () =>
vscode.workspace.fs.readDirectory(root).then(
(items) => {
const stale = trimEntries(items.map(([name]) => ({ path: name })))
return Promise.all(
stale.map((name) =>
Promise.resolve(vscode.workspace.fs.delete(vscode.Uri.joinPath(root, name), { recursive: true })).then(
undefined,
(err: unknown) => {
console.warn("[Kilo New] KiloProvider: Failed to delete stale preview:", err)
},
),
),
)
},
() => [],
)
const open = () =>
vscode.commands
.executeCommand(...getPreviewCommand(uri))
.then(undefined, () => vscode.commands.executeCommand("vscode.open", uri))
void vscode.workspace.fs
.createDirectory(root)
.then(() => vscode.workspace.fs.writeFile(uri, img.data))
.then(() => clean())
.then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err))
}
/**
* Handle openFile request from the webview — open a file in the VS Code editor.
* Resolves relative paths against the current session's directory (which may be
@@ -66,6 +66,7 @@ export class AgentManagerProvider implements vscode.Disposable {
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
) {
this.outputChannel = vscode.window.createOutputChannel("Kilo Agent Manager")
this.terminalManager = new SessionTerminalManager(
@@ -147,7 +148,7 @@ export class AgentManagerProvider implements vscode.Disposable {
panel.webview.html = this.getHtml(panel.webview)
this.provider = new KiloProvider(this.extensionUri, this.connectionService)
this.provider = new KiloProvider(this.extensionUri, this.connectionService, this.context)
this.provider.attachToWebview(panel.webview, {
onBeforeMessage: (msg) => this.onMessage(msg),
})
@@ -232,6 +233,9 @@ export class AgentManagerProvider implements vscode.Disposable {
this.openWorktreeDirectory(m.worktreeId)
return null
}
if (m.type === "previewImage") {
return msg
}
if (m.type === "agentManager.showExistingLocalTerminal") {
this.terminalManager.syncLocalOnSessionSwitch()
return null
@@ -379,6 +379,12 @@ interface GenericOpenFileIn {
column?: number
}
interface PreviewImageIn {
type: "previewImage"
dataUrl: string
filename: string
}
interface LoadMessagesIn {
type: "loadMessages"
sessionID: string
@@ -434,6 +440,7 @@ export type AgentManagerInMessage =
| StopDiffWatchIn
| OpenFileIn
| GenericOpenFileIn
| PreviewImageIn
| LoadMessagesIn
| ClearSessionIn
| AbortIn
+1 -1
View File
@@ -47,7 +47,7 @@ export function activate(context: vscode.ExtensionContext) {
)
// Create Agent Manager provider for editor panel
const agentManagerProvider = new AgentManagerProvider(context.extensionUri, connectionService)
const agentManagerProvider = new AgentManagerProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(agentManagerProvider)
// Register serializer so Agent Manager restores when VS Code restarts
+83
View File
@@ -0,0 +1,83 @@
import * as path from "path"
const IMAGE_PREVIEW_ID = "imagePreview.previewEditor"
const PREVIEW_DIR = "image-preview"
const PREVIEW_LIMIT = 20
type Preview = {
data: Uint8Array
ext: string
name: string
}
export function parseImage(dataUrl: string, filename: string): Preview | null {
const sep = dataUrl.indexOf(",")
if (sep === -1) return null
const head = dataUrl.slice(0, sep)
const mime = head.match(/^data:(image\/[A-Za-z0-9.+-]+);base64$/)?.[1]
if (!mime) return null
const data = parseBase64(dataUrl.slice(sep + 1))
if (!data) return null
const ext = getExt(mime)
const name = buildName(filename, ext)
return { data, ext, name }
}
export function buildPreviewPath(name: string, now: number): string {
return path.posix.join(PREVIEW_DIR, `${now}-${name}`)
}
export function getPreviewCommand(uri: {
toString(): string
}): [string, { resource: { toString(): string }; size: "contain" }] {
return [IMAGE_PREVIEW_ID, { resource: uri, size: "contain" }]
}
export function getPreviewDir(): string {
return PREVIEW_DIR
}
export function trimEntries<T extends { path: string }>(items: T[], limit = PREVIEW_LIMIT): string[] {
if (items.length <= limit) return []
return items
.slice()
.sort((a, b) => a.path.localeCompare(b.path))
.slice(0, items.length - limit)
.map((item) => item.path)
}
function parseBase64(value: string): Uint8Array | null {
try {
return Buffer.from(value, "base64")
} catch {
return null
}
}
function getExt(mime: string): string {
const raw = mime.split("/")[1] ?? "png"
if (raw === "jpeg") return "jpg"
if (raw === "svg+xml") return "svg"
return raw
}
function buildName(filename: string, ext: string): string {
const raw = path.basename(filename || "image")
const item = path.parse(raw)
const base = sanitize(item.name) || "image"
const tail = sanitize(item.ext.replace(/^\./, ""))
const clean = tail ? `${base}.${tail}` : base
if (path.extname(clean)) return clean
return `${clean}.${ext}`
}
function sanitize(value: string): string {
return value
.replace(/[^A-Za-z0-9._-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
}
@@ -44,6 +44,11 @@ export function sessionToWebview(session: Session) {
title: session.title,
createdAt: new Date(session.time.created).toISOString(),
updatedAt: new Date(session.time.updated).toISOString(),
// Use null (not undefined) so the value survives postMessage JSON serialization.
// Without this, unrevert responses lose the revert key entirely and the
// SolidJS store merge never clears the existing revert state.
revert: session.revert ?? null,
summary: session.summary ?? null,
}
}
@@ -162,6 +167,7 @@ export type WebviewMessage =
sessionID: string
toolName: string
patterns: string[]
always: string[]
args: Record<string, unknown>
message: string
tool?: { messageID: string; callID: string }
@@ -174,6 +180,7 @@ export type WebviewMessage =
| { type: "permissionError"; permissionID: string }
| { type: "sessionCreated"; session: ReturnType<typeof sessionToWebview> }
| { type: "sessionUpdated"; session: ReturnType<typeof sessionToWebview> }
| { type: "messageRemoved"; sessionID: string; messageID: string }
| null
export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | undefined): WebviewMessage {
@@ -209,6 +216,14 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
},
}
}
case "message.removed": {
const props = event.properties as { sessionID: string; messageID: string }
return {
type: "messageRemoved",
sessionID: props.sessionID,
messageID: props.messageID,
}
}
case "session.status": {
const info = event.properties.status
return {
@@ -226,6 +241,7 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
sessionID: event.properties.sessionID,
toolName: event.properties.permission,
patterns: event.properties.patterns ?? [],
always: event.properties.always ?? [],
args: event.properties.metadata,
message: `Permission required: ${event.properties.permission}`,
tool: event.properties.tool,
@@ -20,13 +20,10 @@ export class AutocompleteModel {
private connectionService: KiloConnectionService | null = null
public profileName: string | null = null
public profileType: string | null = null
public loaded = false
public hasKilocodeProfileWithNoBalance = false
constructor(connectionService?: KiloConnectionService) {
if (connectionService) {
this.connectionService = connectionService
this.loaded = true
}
}
@@ -37,21 +34,6 @@ export class AutocompleteModel {
this.connectionService = service
}
/**
* Load model configuration.
* Returns true if the connection service is available.
*/
public async reload(): Promise<boolean> {
this.loaded = true
if (this.connectionService) {
const state = this.connectionService.getConnectionState()
return state === "connected"
}
return false
}
public supportsFim(): boolean {
return true
}
@@ -18,7 +18,6 @@ export interface AutocompleteServiceSettings {
provider?: string
model?: string
snoozeUntil?: number
hasKilocodeProfileWithNoBalance?: boolean
}
function readSettings(): AutocompleteServiceSettings {
@@ -59,6 +58,7 @@ export class AutocompleteServiceManager {
public readonly codeActionProvider: AutocompleteCodeActionProvider
public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider
private inlineCompletionProviderDisposable: vscode.Disposable | null = null
private unsubscribeState: (() => void) | null = null
constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) {
if (AutocompleteServiceManager._instance) {
@@ -86,6 +86,12 @@ export class AutocompleteServiceManager {
new AutocompleteTelemetry(),
)
// Reload when CLI backend connection state changes so autocomplete
// picks up the connected state even if it wasn't ready at startup.
this.unsubscribeState = connectionService.onStateChange(() => {
void this.load()
})
void this.load()
}
@@ -97,7 +103,6 @@ export class AutocompleteServiceManager {
}
public async load() {
await this.model.reload()
this.settings = readSettings()
await this.updateGlobalContext()
@@ -119,12 +124,12 @@ export class AutocompleteServiceManager {
return
}
// Register classic provider
// Register classic provider (tracked via this.inlineCompletionProviderDisposable,
// not context.subscriptions, so re-registration on reconnect doesn't leak)
this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider(
{ scheme: "file" },
this.inlineCompletionProvider,
)
this.context.subscriptions.push(this.inlineCompletionProviderDisposable)
}
public async disable() {
@@ -237,11 +242,6 @@ export class AutocompleteServiceManager {
const document = editor.document
// Ensure model is loaded
if (!this.model.loaded) {
await this.load()
}
// Call the inline completion provider directly with manual trigger context
const position = editor.selection.active
const context: vscode.InlineCompletionContext = {
@@ -293,22 +293,16 @@ export class AutocompleteServiceManager {
})
}
private getCurrentModelName(): string | undefined {
if (!this.model.loaded) {
return
}
private getCurrentModelName(): string {
return this.model.getModelName()
}
private getCurrentProviderName(): string | undefined {
if (!this.model.loaded) {
return
}
private getCurrentProviderName(): string {
return this.model.getProviderDisplayName()
}
private hasNoUsableProvider(): boolean {
return this.model.loaded && !this.model.hasValidCredentials() && !this.model.hasKilocodeProfileWithNoBalance
return !this.model.hasValidCredentials()
}
private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void {
@@ -328,7 +322,6 @@ export class AutocompleteServiceManager {
model: this.getCurrentModelName(),
provider: this.getCurrentProviderName(),
profileName: this.model.profileName,
hasKilocodeProfileWithNoBalance: this.model.hasKilocodeProfileWithNoBalance,
hasNoUsableProvider: this.hasNoUsableProvider(),
totalSessionCost: this.sessionCost,
completionCount: this.completionCount,
@@ -360,6 +353,10 @@ export class AutocompleteServiceManager {
this.snoozeTimer = null
}
// Unsubscribe from connection state changes
this.unsubscribeState?.()
this.unsubscribeState = null
// Dispose inline completion provider registration
if (this.inlineCompletionProviderDisposable) {
this.inlineCompletionProviderDisposable.dispose()
@@ -86,22 +86,12 @@ export class AutocompleteStatusBar {
}
public render() {
if (this.props.hasKilocodeProfileWithNoBalance) {
return this.renderNoCreditsError()
}
if (this.props.hasNoUsableProvider) {
return this.renderNoUsableProviderError()
}
return this.renderDefault()
}
private renderNoCreditsError() {
this.statusBar.text = t("kilocode:autocomplete.statusBar.warning")
this.statusBar.tooltip = this.createMarkdownTooltip(
t("kilocode:autocomplete.statusBar.tooltip.noCredits", { command: SETTINGS_COMMAND }),
)
}
private renderNoUsableProviderError() {
this.statusBar.text = t("kilocode:autocomplete.statusBar.warning")
this.statusBar.tooltip = this.createMarkdownTooltip(
@@ -1,732 +1,169 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { describe, it, expect, vi } from "vitest"
import { AutocompleteModel } from "../AutocompleteModel"
import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager"
import { AUTOCOMPLETE_PROVIDER_MODELS } from "../utils/kilocode-utils"
import * as apiIndex from "../../../api"
import { TelemetryService } from "../../../../packages/telemetry/src/TelemetryService"
import type { KiloConnectionService } from "../../cli-backend"
function createMockConnectionService(state: "connecting" | "connected" | "disconnected" | "error" = "connected") {
return {
getConnectionState: vi.fn().mockReturnValue(state),
getClient: vi.fn().mockReturnValue({
kilo: {
fim: vi.fn(),
},
}),
onStateChange: vi.fn().mockReturnValue(() => {}),
} as unknown as KiloConnectionService
}
describe("AutocompleteModel", () => {
let mockProviderSettingsManager: ProviderSettingsManager
beforeEach(() => {
mockProviderSettingsManager = {
listConfig: vi.fn(),
getProfile: vi.fn(),
} as any
describe("constructor", () => {
it("defaults profileName and profileType to null", () => {
const model = new AutocompleteModel()
expect(model.profileName).toBeNull()
expect(model.profileType).toBeNull()
})
})
describe("reload", () => {
it("sorts profiles by supportedProviders index order", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "3", name: "profile3", apiProvider: supportedProviders[2] },
{ id: "1", name: "profile1", apiProvider: supportedProviders[0] },
{ id: "2", name: "profile2", apiProvider: supportedProviders[1] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "profile1",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
describe("setConnectionService", () => {
it("sets the connection service after construction", () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "1" })
})
it("filters out profiles without apiProvider", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "profile1", apiProvider: undefined },
{ id: "2", name: "profile2", apiProvider: supportedProviders[0] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "2",
name: "profile2",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" })
})
it("filters out profiles with unsupported apiProvider", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "profile1", apiProvider: "unsupported" },
{ id: "2", name: "profile2", apiProvider: supportedProviders[0] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "2",
name: "profile2",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" })
})
it("handles empty profile list", async () => {
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([])
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).not.toHaveBeenCalled()
expect(model.hasValidCredentials()).toBe(false)
expect(result).toBe(false)
})
it("returns true when profile found", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [{ id: "1", name: "mistral-profile", apiProvider: "mistral" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "mistral-profile",
apiProvider: "mistral",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
const connection = createMockConnectionService("connected")
model.setConnectionService(connection)
expect(model.hasValidCredentials()).toBe(true)
})
})
describe("provider usability", () => {
beforeEach(() => {
// Mock fetch globally for these tests
vi.stubGlobal("fetch", vi.fn())
// Mock TelemetryService
const mockTelemetryService = {
captureEvent: vi.fn(),
captureException: vi.fn(),
isTelemetryEnabled: vi.fn().mockReturnValue(false),
shutdown: vi.fn(),
}
vi.spyOn(TelemetryService, "hasInstance").mockReturnValue(true)
vi.spyOn(TelemetryService, "instance", "get").mockReturnValue(mockTelemetryService as any)
describe("hasValidCredentials", () => {
it("returns true when connected", () => {
const connection = createMockConnectionService("connected")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(true)
})
afterEach(() => {
// Restore fetch
vi.unstubAllGlobals()
// Restore TelemetryService mocks
vi.restoreAllMocks()
it("returns false when disconnected", () => {
const connection = createMockConnectionService("disconnected")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
it("should skip kilocode provider when balance is zero and use openrouter instead", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
it("returns false when connecting", () => {
const connection = createMockConnectionService("connecting")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles with tokens
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to return zero balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ data: { balance: 0 } }),
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
it("returns false when in error state", () => {
const connection = createMockConnectionService("error")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
it("returns false without connection service", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should have tried both providers but used openrouter (since kilocode balance is 0)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
expect(model.hasValidCredentials()).toBe(false)
})
})
it("should use kilocode provider when balance is greater than zero", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles with tokens
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to return positive balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ data: { balance: 10.5 } }),
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
describe("supportsFim", () => {
it("always returns true", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should have used kilocode provider (first one with positive balance)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
expect(model.supportsFim()).toBe(true)
})
})
it("should handle kilocode provider with no token", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles - kilocode without token
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "", // No token
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to handle the no-token case
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
// This should not be called since there's no token
return {
ok: false,
status: 401,
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
describe("getModelName", () => {
it("returns the default model", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should skip kilocode (no token) and use openrouter
expect(result).toBe(true)
expect(model.loaded).toBe(true)
})
it("should set hasKilocodeProfileWithNoBalance when kilocode profile exists but has no balance", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// Mock fetch to return zero balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 0 }),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should not find a usable provider
expect(result).toBe(false)
expect(model.loaded).toBe(true)
// Should have set the flag indicating kilocode profile exists but has no balance
expect(model.hasKilocodeProfileWithNoBalance).toBe(true)
})
it("should not set hasKilocodeProfileWithNoBalance when kilocode profile has balance", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// Mock fetch to return positive balance for kilocode and valid models response
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 10.5 }),
} as any
}
// For OpenRouter/Kilocode models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({
data: [
{
id: "mistralai/codestral-2508",
name: "Codestral",
context_length: 32000,
pricing: { prompt: "0.0001", completion: "0.0003" },
},
],
}),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should find a usable provider
expect(result).toBe(true)
expect(model.loaded).toBe(true)
// Should not have set the flag
expect(model.hasKilocodeProfileWithNoBalance).toBe(false)
})
it("should clear hasKilocodeProfileWithNoBalance on reload", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// First reload: zero balance
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 0 }),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.hasKilocodeProfileWithNoBalance).toBe(true)
// Second reload: positive balance with valid models response
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 10.5 }),
} as any
}
// For OpenRouter/Kilocode models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({
data: [
{
id: "mistralai/codestral-2508",
name: "Codestral",
context_length: 32000,
pricing: { prompt: "0.0001", completion: "0.0003" },
},
],
}),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
await model.reload(mockProviderSettingsManager)
// Flag should be cleared after reload with positive balance
expect(model.hasKilocodeProfileWithNoBalance).toBe(false)
expect(model.getModelName()).toBe("mistralai/codestral-2508")
})
})
describe("getProviderDisplayName", () => {
it("returns undefined when no provider is loaded", () => {
it("returns Kilo Gateway", () => {
const model = new AutocompleteModel()
expect(model.getProviderDisplayName()).toBeUndefined()
})
it("returns provider name from API handler when provider is loaded", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [{ id: "1", name: "profile1", apiProvider: supportedProviders[0] }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "profile1",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
// Mock buildApiHandler to return a handler with providerName
const mockApiHandler = {
providerName: "Mistral",
getModel: vi.fn().mockReturnValue({ id: "mistral-model", info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
const providerName = model.getProviderDisplayName()
expect(providerName).toBeTruthy()
expect(typeof providerName).toBe("string")
expect(providerName).toBe("Mistral")
// Restore the spy
vi.restoreAllMocks()
})
describe("profile information", () => {
it("returns null for profile name when no profile is loaded", () => {
const model = new AutocompleteModel()
expect(model.profileName).toBeNull()
})
it("returns null for profile type when no profile is loaded", () => {
const model = new AutocompleteModel()
expect(model.profileType).toBeNull()
})
it("stores and returns profile name after loading", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{
id: "1",
name: "My Autocomplete Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Autocomplete Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBe("My Autocomplete Profile")
})
it("stores and returns profile type after loading", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileType).toBe("autocomplete")
})
it("clears profile information on cleanup", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBe("My Profile")
expect(model.profileType).toBe("autocomplete")
// Reload with empty profiles to trigger cleanup
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([])
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBeNull()
expect(model.profileType).toBeNull()
})
expect(model.getProviderDisplayName()).toBe("Kilo Gateway")
})
})
describe("reload model override behavior", () => {
beforeEach(() => {
// Mock buildApiHandler to return a handler with getModel
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: "test-model", info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
})
afterEach(() => {
vi.restoreAllMocks()
})
it("should use custom model for explicit autocomplete profiles", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const customModelId = "custom-autocomplete-model"
const profiles = [
{
id: "1",
name: "My Autocomplete Profile",
apiProvider: provider,
profileType: "autocomplete",
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Autocomplete Profile",
apiProvider: provider,
profileType: "autocomplete",
mistralApiKey: "test-key",
apiModelId: customModelId, // Custom model set by user
} as any)
// Mock buildApiHandler to return the custom model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: customModelId, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
describe("generateFimResponse", () => {
it("throws when connection service is not available", async () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should use the custom model from the profile
const modelName = model.getModelName()
expect(modelName).toBe(customModelId)
await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow(
"Connection service is not available",
)
})
it("should override model for non-autocomplete profiles", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider)
const profiles = [
{
id: "1",
name: "My Chat Profile",
apiProvider: provider,
profileType: "chat", // Not an autocomplete profile
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Chat Profile",
apiProvider: provider,
profileType: "chat",
mistralApiKey: "test-key",
apiModelId: "custom-chat-model", // This should be overridden
} as any)
// Mock buildApiHandler to return the overridden model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should be overridden with the default autocomplete model
const modelName = model.getModelName()
expect(modelName).toBe(defaultAutocompleteModel)
it("throws when not connected", async () => {
const connection = createMockConnectionService("disconnected")
const model = new AutocompleteModel(connection)
await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow(
"CLI backend is not connected (state: disconnected)",
)
})
it("should override model for profiles without profileType", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider)
const profiles = [
it("streams chunks and returns metadata", async () => {
const chunks = [
{ choices: [{ delta: { content: "hello" } }] },
{
id: "1",
name: "My Generic Profile",
apiProvider: provider,
// No profileType specified
choices: [{ delta: { content: " world" } }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
cost: 0.001,
},
] as any
]
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Generic Profile",
apiProvider: provider,
mistralApiKey: "test-key",
apiModelId: "custom-model", // This should be overridden
} as any)
const connection = createMockConnectionService("connected")
const client = (connection as any).getClient()
client.kilo.fim.mockResolvedValue({
stream: (async function* () {
for (const chunk of chunks) yield chunk
})(),
})
// Mock buildApiHandler to return the overridden model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel(connection)
const received: string[] = []
const result = await model.generateFimResponse("prefix", "suffix", (text) => received.push(text))
expect(received).toEqual(["hello", " world"])
expect(result).toEqual({
cost: 0.001,
inputTokens: 10,
outputTokens: 5,
cacheWriteTokens: 0,
cacheReadTokens: 0,
})
})
it("passes model parameters to fim call", async () => {
const connection = createMockConnectionService("connected")
const client = (connection as any).getClient()
client.kilo.fim.mockResolvedValue({
stream: (async function* () {})(),
})
const model = new AutocompleteModel(connection)
const signal = new AbortController().signal
await model.generateFimResponse("pre", "suf", vi.fn(), signal)
expect(client.kilo.fim).toHaveBeenCalledWith(
{
prefix: "pre",
suffix: "suf",
model: "mistralai/codestral-2508",
maxTokens: 256,
temperature: 0.2,
},
{ signal },
)
})
})
describe("generateResponse", () => {
it("throws because FIM is the primary strategy", async () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should be overridden with the default autocomplete model
const modelName = model.getModelName()
expect(modelName).toBe(defaultAutocompleteModel)
await expect(model.generateResponse("system", "user", vi.fn())).rejects.toThrow(
"Chat-based completions are not supported via CLI backend",
)
})
})
})
@@ -63,13 +63,8 @@ vi.mock("vscode", () => {
vi.mock("../AutocompleteModel", () => {
class AutocompleteModel {
public loaded = false
public profileName = "test-profile"
public async reload(): Promise<void> {
this.loaded = true
}
public getModelName(): string {
return "test-model"
}
@@ -169,7 +164,9 @@ async function createManager(): Promise<AutocompleteServiceManager> {
postStateToWebview: vi.fn().mockResolvedValue(undefined),
}
const manager = new AutocompleteServiceManager(context, cline as any)
const connection = { onStateChange: vi.fn().mockReturnValue(() => {}), ...cline }
const manager = new AutocompleteServiceManager(context, connection as any)
await manager.load()
@@ -15,10 +15,6 @@ export class ChatTextAreaAutocomplete {
this.telemetry = new AutocompleteTelemetry("chat-textarea")
}
async initialize(): Promise<boolean> {
return this.model.reload()
}
async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> {
const startTime = Date.now()
@@ -29,13 +25,6 @@ export class ChatTextAreaAutocomplete {
provider: this.model.getProviderDisplayName(),
}
if (!this.model.loaded) {
const loaded = await this.initialize()
if (!loaded) {
return { suggestion: "" }
}
}
// Check if model has valid credentials (but don't require FIM)
if (!this.model.hasValidCredentials()) {
return { suggestion: "" }
@@ -16,7 +16,6 @@ describe("ChatTextAreaAutocomplete", () => {
it("should work with non-FIM models using chat-based completion", async () => {
// Setup: Model without FIM support (like Mistral)
const mockModel = new AutocompleteModel()
mockModel.loaded = true
vi.spyOn(mockModel, "hasValidCredentials").mockReturnValue(true)
vi.spyOn(mockModel, "supportsFim").mockReturnValue(false)
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (معطل)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**لا يوجد رصيد في حسابك**\n\nحساب Kilo Code الخاص بك لا يحتوي على رصيد. لاستخدام الإكمال التلقائي، يرجى إضافة رصيد إلى حسابك.\n\n[فتح الإعدادات]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**لم يتم تكوين نموذج الإكمال التلقائي**\n\nلتمكين الإكمال التلقائي، أضف ملفًا شخصيًا مع أحد هذه المزودين المدعومين: {{providers}}.\n\n[فتح الإعدادات]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "إجمالي تكلفة الجلسة:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desactivat)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No tens crèdits al teu compte**\n\nEl teu compte de Kilo Code no té crèdits. Per utilitzar l'autocompletat, si us plau afegeix crèdits al teu compte.\n\n[Obrir Configuració]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No s'ha configurat cap model d'autocompletat**\n\nPer habilitar l'autocompletat, afegeix un perfil amb un d'aquests proveïdors compatibles: {{providers}}.\n\n[Obrir Configuració]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Cost total de la sessió:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázáno)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Na tvém účtu nejsou žádné kredity**\n\nTvůj účet Kilo Code nemá žádné kredity. Pro použití automatického doplňování prosím přidej kredity na svůj účet.\n\n[Otevřít Nastavení]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Není nakonfigurován žádný model automatického doplňování**\n\nPro povolení automatického doplňování přidej profil s jedním z těchto podporovaných poskytovatelů: {{providers}}.\n\n[Otevřít Nastavení]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relace:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deaktiviert)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Kein Guthaben auf deinem Konto**\n\nDein Kilo Code Konto hat kein Guthaben. Um Autocomplete zu nutzen, füge bitte Guthaben zu deinem Konto hinzu.\n\n[Einstellungen öffnen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Kein Autocomplete-Modell konfiguriert**\n\nUm Autocomplete zu aktivieren, füge ein Profil mit einem dieser unterstützten Anbieter hinzu: {{providers}}.\n\n[Einstellungen öffnen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Sitzungsgesamtkosten:",
@@ -7,8 +7,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabled)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No credits in your account**\n\nYour Kilo Code account has no credits. To use autocomplete, please add credits to your account.\n\n[Open Settings]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No autocomplete model configured**\n\nTo enable autocomplete, add a profile with one of these supported providers: {{providers}}.\n\n[Open Settings]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Session total cost:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deshabilitado)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No hay créditos en tu cuenta**\n\nTu cuenta de Kilo Code no tiene créditos. Para usar el autocompletado, por favor añade créditos a tu cuenta.\n\n[Abrir Configuración]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No hay modelo de autocompletado configurado**\n\nPara habilitar el autocompletado, añade un perfil con uno de estos proveedores compatibles: {{providers}}.\n\n[Abrir Configuración]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo total de la sesión:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (désactivé)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Pas de crédits sur ton compte**\n\nTon compte Kilo Code n'a pas de crédits. Pour utiliser l'autocomplétion, ajoute des crédits à ton compte.\n\n[Ouvrir les Paramètres]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Aucun modèle d'autocomplétion configuré**\n\nPour activer l'autocomplétion, ajoute un profil avec l'un de ces fournisseurs pris en charge : {{providers}}.\n\n[Ouvrir les Paramètres]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Coût total de la session :",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (अक्षम)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**आपके खाते में कोई क्रेडिट नहीं है**\n\nआपके Kilo Code खाते में कोई क्रेडिट नहीं है। ऑटोकम्प्लीट का उपयोग करने के लिए, कृपया अपने खाते में क्रेडिट जोड़ें।\n\n[सेटिंग्स खोलें]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**कोई ऑटोकम्प्लीट मॉडल कॉन्फ़िगर नहीं किया गया**\n\nऑटोकम्प्लीट सक्षम करने के लिए, इन समर्थित प्रदाताओं में से एक के साथ एक प्रोफ़ाइल जोड़ें: {{providers}}।\n\n[सेटिंग्स खोलें]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "सत्र की कुल लागत:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (dinonaktifkan)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Tidak ada kredit di akun kamu**\n\nAkun Kilo Code kamu tidak memiliki kredit. Untuk menggunakan autocomplete, silakan tambahkan kredit ke akun kamu.\n\n[Buka Pengaturan]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Tidak ada model autocomplete yang dikonfigurasi**\n\nUntuk mengaktifkan autocomplete, tambahkan profil dengan salah satu penyedia yang didukung ini: {{providers}}.\n\n[Buka Pengaturan]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Total biaya sesi:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabilitato)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Nessun credito nel tuo account**\n\nIl tuo account Kilo Code non ha crediti. Per usare l'autocompletamento, aggiungi crediti al tuo account.\n\n[Apri Impostazioni]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nessun modello di autocompletamento configurato**\n\nPer abilitare l'autocompletamento, aggiungi un profilo con uno di questi provider supportati: {{providers}}.\n\n[Apri Impostazioni]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo totale della sessione:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(無効)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**アカウントにクレジットがありません**\n\nKilo Code アカウントにクレジットがありません。オートコンプリートを使用するには、アカウントにクレジットを追加してください。\n\n[設定を開く]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**オートコンプリートモデルが設定されていません**\n\nオートコンプリートを有効にするには、これらのサポートされているプロバイダーのいずれかでプロファイルを追加してください: {{providers}}。\n\n[設定を開く]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "セッション合計コスト:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (비활성화됨)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**계정에 크레딧이 없습니다**\n\nKilo Code 계정에 크레딧이 없습니다. 자동완성을 사용하려면 계정에 크레딧을 추가해 주세요.\n\n[설정 열기]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**자동완성 모델이 구성되지 않았습니다**\n\n자동완성을 활성화하려면 다음 지원되는 제공업체 중 하나로 프로필을 추가하세요: {{providers}}.\n\n[설정 열기]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "세션 총 비용:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (uitgeschakeld)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Geen tegoed op je account**\n\nJe Kilo Code account heeft geen tegoed. Om autocomplete te gebruiken, voeg tegoed toe aan je account.\n\n[Instellingen openen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Geen autocomplete model geconfigureerd**\n\nOm autocomplete in te schakelen, voeg een profiel toe met een van deze ondersteunde providers: {{providers}}.\n\n[Instellingen openen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Totale sessiekosten:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (wyłączone)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Brak środków na koncie**\n\nTwoje konto Kilo Code nie ma środków. Aby korzystać z autouzupełniania, dodaj środki do swojego konta.\n\n[Otwórz Ustawienia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nie skonfigurowano modelu autouzupełniania**\n\nAby włączyć autouzupełnianie, dodaj profil z jednym z tych obsługiwanych dostawców: {{providers}}.\n\n[Otwórz Ustawienia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Całkowity koszt sesji:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desabilitado)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Sem créditos na sua conta**\n\nSua conta Kilo Code não tem créditos. Para usar o autocompletar, adicione créditos à sua conta.\n\n[Abrir Configurações]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nenhum modelo de autocompletar configurado**\n\nPara habilitar o autocompletar, adicione um perfil com um destes provedores suportados: {{providers}}.\n\n[Abrir Configurações]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Custo total da sessão:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (отключено)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**На вашем счёте нет кредитов**\n\nНа вашем счёте Kilo Code нет кредитов. Чтобы использовать автодополнение, пожалуйста, пополните счёт.\n\n[Открыть Настройки]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Модель автодополнения не настроена**\n\nЧтобы включить автодополнение, добавьте профиль с одним из поддерживаемых провайдеров: {{providers}}.\n\n[Открыть Настройки]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Общая стоимость сессии:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázané)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Na tvojom účte nie sú žiadne kredity**\n\nTvoj účet Kilo Code nemá žiadne kredity. Pre použitie automatického doplňovania prosím pridaj kredity na svoj účet.\n\n[Otvoriť Nastavenia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nie je nakonfigurovaný žiadny model automatického doplňovania**\n\nPre povolenie automatického doplňovania pridaj profil s jedným z týchto podporovaných poskytovateľov: {{providers}}.\n\n[Otvoriť Nastavenia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relácie:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (ปิดใช้งาน)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**ไม่มีเครดิตในบัญชีของคุณ**\n\nบัญชี Kilo Code ของคุณไม่มีเครดิต หากต้องการใช้การเติมข้อความอัตโนมัติ กรุณาเพิ่มเครดิตในบัญชีของคุณ\n\n[เปิดการตั้งค่า]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**ไม่ได้กำหนดค่าโมเดลการเติมข้อความอัตโนมัติ**\n\nหากต้องการเปิดใช้งานการเติมข้อความอัตโนมัติ ให้เพิ่มโปรไฟล์กับผู้ให้บริการที่รองรับเหล่านี้: {{providers}}\n\n[เปิดการตั้งค่า]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "ค่าใช้จ่ายรวมของเซสชัน:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (devre dışı)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Hesabınızda kredi yok**\n\nKilo Code hesabınızda kredi bulunmuyor. Otomatik tamamlamayı kullanmak için lütfen hesabınıza kredi ekleyin.\n\n[Ayarları Aç]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Otomatik tamamlama modeli yapılandırılmadı**\n\nOtomatik tamamlamayı etkinleştirmek için desteklenen sağlayıcılardan biriyle bir profil ekleyin: {{providers}}.\n\n[Ayarları Aç]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Oturum toplam maliyeti:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (вимкнено)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**На вашому рахунку немає кредитів**\n\nНа вашому рахунку Kilo Code немає кредитів. Щоб використовувати автодоповнення, будь ласка, поповніть рахунок.\n\n[Відкрити Налаштування]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Модель автодоповнення не налаштована**\n\nЩоб увімкнути автодоповнення, додайте профіль з одним із підтримуваних провайдерів: {{providers}}.\n\n[Відкрити Налаштування]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Загальна вартість сесії:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (đã tắt)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Không có tín dụng trong tài khoản của bạn**\n\nTài khoản Kilo Code của bạn không có tín dụng. Để sử dụng tự động hoàn thành, vui lòng thêm tín dụng vào tài khoản của bạn.\n\n[Mở Cài đặt]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Chưa cấu hình mô hình tự động hoàn thành**\n\nĐể bật tự động hoàn thành, hãy thêm hồ sơ với một trong các nhà cung cấp được hỗ trợ sau: {{providers}}.\n\n[Mở Cài đặt]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Tổng chi phí phiên:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已禁用)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**账户余额不足**\n\n你的 Kilo Code 账户没有余额。要使用自动补全功能,请为账户充值。\n\n[打开设置]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**未配置自动补全模型**\n\n要启用自动补全,请添加一个使用以下支持的提供商的配置文件:{{providers}}。\n\n[打开设置]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "会话总费用:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已停用)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**帳戶中沒有額度**\n\nKilo Code 帳戶沒有額度。若要使用 Autocomplete,請為帳戶儲值。\n\n[開啟設定]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**未設定 Autocomplete 模型**\n\n若要啟用 Autocomplete,請新增一個使用以下支援供應商的設定檔:{{providers}}。\n\n[開啟設定]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "工作階段總費用:",
@@ -137,7 +137,6 @@ export interface AutocompleteStatusBarStateProps {
model?: string
provider?: string
profileName?: string | null
hasKilocodeProfileWithNoBalance?: boolean
hasNoUsableProvider?: boolean
totalSessionCost: number
completionCount: number
@@ -3,9 +3,13 @@ import * as path from "path"
import * as os from "os"
import { execFile } from "child_process"
import { promisify } from "util"
import * as yaml from "yaml"
import type {
MarketplaceItem,
SkillMarketplaceItem,
McpMarketplaceItem,
ModeMarketplaceItem,
McpInstallationMethod,
InstallMarketplaceItemOptions,
InstallResult,
RemoveResult,
@@ -24,9 +28,87 @@ export class MarketplaceInstaller {
): Promise<InstallResult> {
const scope = options.target ?? "project"
if (item.type === "skill") return this.installSkill(item, scope, workspace)
return { success: false, slug: item.id, error: `${item.type} installation not yet implemented` }
if (item.type === "mcp") return this.installMcp(item, options, scope, workspace)
return this.installMode(item, scope, workspace)
}
// ── MCP ─────────────────────────────────────────────────────────────
async installMcp(
item: McpMarketplaceItem,
options: InstallMarketplaceItemOptions,
scope: "project" | "global",
workspace?: string,
): Promise<InstallResult> {
if (scope === "project" && !workspace) {
return { success: false, slug: item.id, error: "No workspace directory for project-scope install" }
}
const config = await this.readConfig(scope, workspace)
if (!config.mcp) config.mcp = {}
if (config.mcp[item.id]) {
return { success: false, slug: item.id, error: "MCP server already installed. Remove it first." }
}
const content = this.resolveMcpContent(item, options)
if (!content) {
return { success: false, slug: item.id, error: "No installation content for MCP server" }
}
try {
config.mcp[item.id] = this.buildMcpEntry(content, options.parameters)
} catch (err) {
return { success: false, slug: item.id, error: `Invalid MCP config: ${err}` }
}
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
private resolveMcpContent(item: McpMarketplaceItem, options: InstallMarketplaceItemOptions): string | undefined {
if (typeof item.content === "string") return item.content
if (!Array.isArray(item.content) || item.content.length === 0) return undefined
const name = options.parameters?.__method as string | undefined
if (name) {
const found = item.content.find((m: McpInstallationMethod) => m.name === name)
if (found) return found.content
}
return item.content[0].content
}
private buildMcpEntry(content: string, params?: Record<string, unknown>): Record<string, unknown> {
const filtered = Object.fromEntries(Object.entries(params ?? {}).filter(([k]) => k !== "__method"))
const replaced = Object.keys(filtered).length > 0 ? substituteParams(content, filtered) : content
return JSON.parse(replaced)
}
// ── Mode ────────────────────────────────────────────────────────────
async installMode(
item: ModeMarketplaceItem,
scope: "project" | "global",
workspace?: string,
): Promise<InstallResult> {
if (scope === "project" && !workspace) {
return { success: false, slug: item.id, error: "No workspace directory for project-scope install" }
}
const config = await this.readConfig(scope, workspace)
if (!config.agent) config.agent = {}
if (config.agent[item.id]) {
return { success: false, slug: item.id, error: "Mode already installed. Remove it first." }
}
config.agent[item.id] = convertModeToAgent(item.content)
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
// ── Skill ───────────────────────────────────────────────────────────
async installSkill(
item: SkillMarketplaceItem,
scope: "project" | "global",
@@ -106,9 +188,34 @@ export class MarketplaceInstaller {
}
}
// ── Remove ──────────────────────────────────────────────────────────
async remove(item: MarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
if (item.type === "skill") return this.removeSkill(item, scope, workspace)
return { success: false, slug: item.id, error: `${item.type} removal not yet implemented` }
if (item.type === "mcp") return this.removeMcp(item, scope, workspace)
return this.removeMode(item, scope, workspace)
}
async removeMcp(item: McpMarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
const config = await this.readConfig(scope, workspace)
if (!config.mcp?.[item.id]) {
return { success: true, slug: item.id }
}
delete config.mcp[item.id]
if (Object.keys(config.mcp).length === 0) delete config.mcp
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
async removeMode(item: ModeMarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
const config = await this.readConfig(scope, workspace)
if (!config.agent?.[item.id]) {
return { success: true, slug: item.id }
}
delete config.agent[item.id]
if (Object.keys(config.agent).length === 0) delete config.agent
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
async removeSkill(
@@ -136,6 +243,32 @@ export class MarketplaceInstaller {
return { success: false, slug: item.id, error: String(err) }
}
}
// ── Config helpers ──────────────────────────────────────────────────
private async readConfig(
scope: "project" | "global",
workspace?: string,
): Promise<Record<string, Record<string, unknown>>> {
const filepath = this.paths.configPath(scope, workspace)
try {
const content = await fs.readFile(filepath, "utf-8")
return JSON.parse(content)
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") return {}
throw err
}
}
private async writeConfig(
scope: "project" | "global",
workspace: string | undefined,
config: Record<string, unknown>,
): Promise<void> {
const filepath = this.paths.configPath(scope, workspace)
await fs.mkdir(path.dirname(filepath), { recursive: true })
await fs.writeFile(filepath, JSON.stringify(config, null, 2) + "\n", "utf-8")
}
}
// ── Helpers ─────────────────────────────────────────────────────────
@@ -145,6 +278,66 @@ function isSafeId(id: string): boolean {
return /^[\w\-@.]+$/.test(id)
}
// Group name → opencode permission key mapping (mirrors ModesMigrator)
const GROUP_PERMISSIONS: Record<string, string> = {
read: "read",
edit: "edit",
browser: "bash",
command: "bash",
mcp: "mcp",
}
const ALL_PERMISSIONS = ["read", "edit", "bash", "mcp"]
function convertModeToAgent(content: string): Record<string, unknown> {
const mode = yaml.parse(content) as Record<string, unknown>
const groups = (mode.groups ?? []) as Array<string | [string, Record<string, unknown>]>
const permission: Record<string, unknown> = {}
const allowed = new Set<string>()
for (const group of groups) {
if (typeof group === "string") {
const key = GROUP_PERMISSIONS[group] ?? group
allowed.add(key)
permission[key] = "allow"
} else if (Array.isArray(group)) {
const [name, cfg] = group
const key = GROUP_PERMISSIONS[name] ?? name
allowed.add(key)
permission[key] = cfg?.fileRegex ? { [String(cfg.fileRegex)]: "allow", "*": "deny" } : "allow"
}
}
for (const perm of ALL_PERMISSIONS) {
if (!allowed.has(perm)) permission[perm] = "deny"
}
const prompt = [mode.roleDefinition, mode.customInstructions].filter(Boolean).join("\n\n")
return {
mode: "primary",
description: mode.description ?? mode.whenToUse ?? mode.name,
prompt,
permission,
}
}
function escapeJsonValue(raw: string): string {
return raw
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t")
}
function substituteParams(template: string, params: Record<string, unknown>): string {
let result = template
for (const [key, value] of Object.entries(params)) {
const escaped = escapeJsonValue(String(value ?? ""))
result = result.replaceAll(`{{${key}}}`, escaped)
result = result.replaceAll(`\${${key}}`, escaped)
}
return result
}
async function findEscapedPaths(dir: string): Promise<string[]> {
const resolved = path.resolve(dir)
const escaped: string[] = []
@@ -32,8 +32,10 @@ export enum TelemetryEventName {
PROMPT_ENHANCED = "Prompt Enhanced",
// Marketplace
MARKETPLACE_INSTALL_BUTTON_CLICKED = "Marketplace Install Button Clicked",
MARKETPLACE_ITEM_INSTALLED = "Marketplace Item Installed",
MARKETPLACE_ITEM_REMOVED = "Marketplace Item Removed",
MARKETPLACE_TAB_VIEWED = "Marketplace Tab Viewed",
// Account & Auth
ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked",
@@ -160,22 +160,6 @@ test.describe("Permission Dock Dropdown — write", () => {
const root = page.locator("#storybook-root")
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-expanded-pending.png"])
})
test("rules expanded — mixed decisions", async ({ page }) => {
await page.goto(storyUrl(STORY_ID), { waitUntil: "load" })
await disableAnimations(page)
await page.waitForSelector("#storybook-root *", { state: "attached" })
await openDropdown(page)
// Approve first rule, deny second (dispatchEvent bypasses tooltip overlays)
const approveButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="approve"]')
const denyButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="deny"]')
await approveButtons.first().dispatchEvent("click")
await denyButtons.nth(1).dispatchEvent("click")
const root = page.locator("#storybook-root")
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-rules-mixed.png"])
})
})
// ---------------------------------------------------------------------------
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ba76e2d5b993e999914fd0538650728adf78f7ad70978d8ee9982525f6ab5e8
size 17626
oid sha256:3ef8ec5886054bf0e7ae288c66ce5175204e74728c3e37f3669c551be98755b7
size 12476
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29872fa36a6a6ba9e20765f92380b86f9a91beb9702b3cdb95914ea46a2cf40c
size 17833
oid sha256:54ff3bd3916245aa1dc65384db4a0b5908b4d78cc3700157112f1be63ce054f9
size 12663
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce915e449cdec411c00df613ffc1017f95dba69737e0474fc8c483ea9f284026
size 17872
oid sha256:9edcfe9f76314110e9c1795ee0008ca35187ce28a19c86cfcf1f8bf499fe151c
size 12714
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b39675360da34fc6cb9c1ff4e51afeff79417ca4baa88816967d2b0a4ece858f
size 18043
oid sha256:4e314513c9b4e964ea3b9a7cdbfc561e57eab2fa867aadfd370d1c0ad68c11b8
size 12903
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3b7d96f123e880b1395d160d17a48f385ef59344326c44d7cd13b342d2ca49e
size 24193
oid sha256:a3b9925b5424dd81e0fd60dc3cc4307730abdc010cccd73e7243292880dd6b09
size 16833
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:739b8eb94e5cde39e4127a6ed3c499392174a380812e2e3497965643cac3dee9
size 22265
oid sha256:ef1d55223be06fd9d16566bc80a525c84c933a337ee4ccf2c41809e4984571b6
size 17276
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:62ef30b05bf2ec47a7ddd02a03f5f1cded5ced9cf9332dd3bd50d6809bcce96d
size 18447
oid sha256:5fb585654180101c29bd4e7efbc7129bf29e8681bc764cdae0d8233fe302a29e
size 12711
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f9864861136b578c4e46ece7dae5a139ada7e7be3d219462ef3a549b94239ec
size 18648
oid sha256:c0702dda15977143eeac1557040a583946f2fed4b1610c97a772cff4ae3f2482
size 12842
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:514803fa46f43ff58d8d403e630afc5c01a8841ac420d406755515bceb38beae
size 23132
oid sha256:9784fcf6612167075eb796887c9ac6ad8ab1596ef03dd0ace2b159f82dcb3675
size 17978
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:578f6daa54a25fba38b049d623e2b12069b0cbe1c5e094031d8a0b0af0a56f52
size 23966
oid sha256:606cc8d90900a26682852c9eb14d24305f31672ecd40265954b7b85cca7d3c48
size 18850
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91ba11c0a81ced5dfe824a4c9e8fdcbaebaaddc660ef2b245df56903e180e4b7
size 18949
oid sha256:ab71bff42d0bd60c84f75cc04d91fdae8516667913152a0ae4e792268368c9d1
size 13844
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f8bc26191620881314eff9ac921abba4e201c5af7ec8d222e405fdee9ddae9a
size 17909
oid sha256:8d37fd5942ec471c7f534b7b65ac568cdde7cd1b7f3542c50a72500ac3823cae
size 12827
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce112e9762168271fd8b7b4316b740008269e9d248a668887adce3dde13478f0
size 18362
oid sha256:04377b0ddbaa0e2f4f126adff431db902fbff65ce653f2bc59434d150a833578
size 15638
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d499d73467c37052d1277deefdd419e370a8239d7e117806d8442b2b9cc851a7
size 18886
@@ -51,6 +51,14 @@ const mockVscode = {
update: async () => {},
}),
asRelativePath: (pathOrUri: string) => pathOrUri,
fs: {
createDirectory: async () => {},
writeFile: async () => {},
readFile: async () => new Uint8Array(),
readDirectory: async () => [],
delete: async () => {},
stat: async () => ({ type: 1, ctime: 0, mtime: 0, size: 0 }),
},
},
window: {
activeTextEditor: undefined,
@@ -0,0 +1,86 @@
/**
* Architecture test: package.json source command sync
*
* Every command declared in package.json contributes.commands must have a
* matching registerCommand() call somewhere in src/. A declaration without
* an implementation causes a silent "command not found" error at runtime
* that is hard to diagnose VS Code shows no warning at activation time.
*/
import { describe, it, expect } from "bun:test"
import fs from "node:fs"
import path from "node:path"
const ROOT = path.resolve(import.meta.dir, "../..")
const PKG_JSON_FILE = path.join(ROOT, "package.json")
const SRC_DIR = path.join(ROOT, "src")
function readSrcFiles(dir: string): string {
const parts: string[] = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
parts.push(readSrcFiles(full))
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".spec.ts")) {
parts.push(fs.readFileSync(full, "utf-8"))
}
}
return parts.join("\n")
}
describe("Extension — package.json command sync", () => {
const pkg = JSON.parse(fs.readFileSync(PKG_JSON_FILE, "utf-8"))
const declared: string[] = pkg.contributes?.commands?.map((c: { command: string }) => c.command) ?? []
const source = readSrcFiles(SRC_DIR)
// Extract command IDs that appear in registerCommand() calls specifically.
// This avoids false positives from executeCommand() or other string references.
const registered = new Set([...source.matchAll(/registerCommand\s*\(\s*["']([^"']+)["']/g)].map((m) => m[1]))
/**
* Every command declared in package.json must be registered via registerCommand()
* somewhere in src/. A bare string match would accept executeCommand() references,
* which don't actually register a handler.
*
* Commands registered via template literals (e.g. jumpTo${i}) are detected by
* checking the dynamic registerCommand pattern in source instead.
*/
it("every contributes.commands entry has a registerCommand() call", () => {
// Commands generated via template literals can't be extracted by regex,
// so verify the dynamic registration pattern exists in source instead.
const dynamic: Record<string, string> = {
"kilo-code.new.agentManager.jumpTo": "registerCommand(`kilo-code.new.agentManager.jumpTo${",
}
const missing: string[] = []
for (const cmd of declared) {
const entry = Object.entries(dynamic).find(([prefix]) => cmd.startsWith(prefix))
if (entry) {
const [, pattern] = entry
if (!source.includes(pattern)) missing.push(`${cmd} (dynamic pattern not found)`)
continue
}
if (!registered.has(cmd)) missing.push(cmd)
}
expect(
missing,
`Commands declared in package.json but not registered via registerCommand().\n` +
`Add registerCommand("...", ...) or remove the declaration:\n` +
missing.map((m) => ` - ${m}`).join("\n"),
).toEqual([])
})
/**
* All declared commands must use the kilo-code.new. prefix.
* The legacy kilo-code.* namespace (without .new.) belongs to the old
* extension and must not be reintroduced.
*/
it("all declared commands use the kilo-code.new. prefix", () => {
const bad = declared.filter((cmd) => !cmd.startsWith("kilo-code.new."))
expect(
bad,
`Commands without "kilo-code.new." prefix — use the namespaced form:\n` + bad.map((b) => ` - ${b}`).join("\n"),
).toEqual([])
})
})
@@ -0,0 +1,407 @@
/**
* Translation key validation
*
* 1. Ensures every string-literal translation key passed to a t() function
* actually exists in the corresponding English dictionary.
* 2. Ensures every English key has a translation in all other locale files.
*
* Three independent key pools are checked:
* - Webview (sidebar + agent manager): merged from app, ui, kilo-i18n, agent-manager dicts
* - CLI backend (extension-side server-manager): cli-backend/i18n dict
* - Autocomplete (extension-side): autocomplete/i18n dict
*
* Dynamic keys (template literals, variables) are intentionally skipped
* only string literals are validated.
*/
import { describe, it, expect } from "bun:test"
import { Glob } from "bun"
import path from "node:path"
// ── Webview dictionaries ────────────────────────────────────────────────────
// Layer 1: app-local (sidebar)
import { dict as appEn } from "../../webview-ui/src/i18n/en"
import { dict as appZh } from "../../webview-ui/src/i18n/zh"
import { dict as appZht } from "../../webview-ui/src/i18n/zht"
import { dict as appKo } from "../../webview-ui/src/i18n/ko"
import { dict as appDe } from "../../webview-ui/src/i18n/de"
import { dict as appEs } from "../../webview-ui/src/i18n/es"
import { dict as appFr } from "../../webview-ui/src/i18n/fr"
import { dict as appDa } from "../../webview-ui/src/i18n/da"
import { dict as appJa } from "../../webview-ui/src/i18n/ja"
import { dict as appPl } from "../../webview-ui/src/i18n/pl"
import { dict as appRu } from "../../webview-ui/src/i18n/ru"
import { dict as appAr } from "../../webview-ui/src/i18n/ar"
import { dict as appNo } from "../../webview-ui/src/i18n/no"
import { dict as appBr } from "../../webview-ui/src/i18n/br"
import { dict as appTh } from "../../webview-ui/src/i18n/th"
import { dict as appBs } from "../../webview-ui/src/i18n/bs"
// Layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui)
import { dict as uiEn } from "../../../ui/src/i18n/en"
import { dict as uiZh } from "../../../ui/src/i18n/zh"
import { dict as uiZht } from "../../../ui/src/i18n/zht"
import { dict as uiKo } from "../../../ui/src/i18n/ko"
import { dict as uiDe } from "../../../ui/src/i18n/de"
import { dict as uiEs } from "../../../ui/src/i18n/es"
import { dict as uiFr } from "../../../ui/src/i18n/fr"
import { dict as uiDa } from "../../../ui/src/i18n/da"
import { dict as uiJa } from "../../../ui/src/i18n/ja"
import { dict as uiPl } from "../../../ui/src/i18n/pl"
import { dict as uiRu } from "../../../ui/src/i18n/ru"
import { dict as uiAr } from "../../../ui/src/i18n/ar"
import { dict as uiNo } from "../../../ui/src/i18n/no"
import { dict as uiBr } from "../../../ui/src/i18n/br"
import { dict as uiTh } from "../../../ui/src/i18n/th"
import { dict as uiBs } from "../../../ui/src/i18n/bs"
// Layer 3: kilo-i18n overrides
import { dict as kiloEn } from "../../../kilo-i18n/src/en"
import { dict as kiloZh } from "../../../kilo-i18n/src/zh"
import { dict as kiloZht } from "../../../kilo-i18n/src/zht"
import { dict as kiloKo } from "../../../kilo-i18n/src/ko"
import { dict as kiloDe } from "../../../kilo-i18n/src/de"
import { dict as kiloEs } from "../../../kilo-i18n/src/es"
import { dict as kiloFr } from "../../../kilo-i18n/src/fr"
import { dict as kiloDa } from "../../../kilo-i18n/src/da"
import { dict as kiloJa } from "../../../kilo-i18n/src/ja"
import { dict as kiloPl } from "../../../kilo-i18n/src/pl"
import { dict as kiloRu } from "../../../kilo-i18n/src/ru"
import { dict as kiloAr } from "../../../kilo-i18n/src/ar"
import { dict as kiloNo } from "../../../kilo-i18n/src/no"
import { dict as kiloBr } from "../../../kilo-i18n/src/br"
import { dict as kiloTh } from "../../../kilo-i18n/src/th"
import { dict as kiloBs } from "../../../kilo-i18n/src/bs"
// Layer 4: agent manager (locale alignment already tested in agent-manager-i18n-split.test.ts)
import { dict as amEn } from "../../webview-ui/agent-manager/i18n/en"
// ── Extension-side dictionaries ─────────────────────────────────────────────
import { dict as cliEn } from "../../src/services/cli-backend/i18n/en"
import { dict as cliZh } from "../../src/services/cli-backend/i18n/zh"
import { dict as cliZht } from "../../src/services/cli-backend/i18n/zht"
import { dict as cliDe } from "../../src/services/cli-backend/i18n/de"
import { dict as cliEs } from "../../src/services/cli-backend/i18n/es"
import { dict as cliFr } from "../../src/services/cli-backend/i18n/fr"
import { dict as cliDa } from "../../src/services/cli-backend/i18n/da"
import { dict as cliJa } from "../../src/services/cli-backend/i18n/ja"
import { dict as cliKo } from "../../src/services/cli-backend/i18n/ko"
import { dict as cliPl } from "../../src/services/cli-backend/i18n/pl"
import { dict as cliRu } from "../../src/services/cli-backend/i18n/ru"
import { dict as cliAr } from "../../src/services/cli-backend/i18n/ar"
import { dict as cliNo } from "../../src/services/cli-backend/i18n/no"
import { dict as cliBr } from "../../src/services/cli-backend/i18n/br"
import { dict as cliTh } from "../../src/services/cli-backend/i18n/th"
import { dict as cliBs } from "../../src/services/cli-backend/i18n/bs"
import { dict as acEn } from "../../src/services/autocomplete/i18n/en"
// ── Locale maps ─────────────────────────────────────────────────────────────
const ROOT = path.resolve(import.meta.dir, "../..")
const appLocales: Record<string, Record<string, string>> = {
en: appEn,
zh: appZh,
zht: appZht,
ko: appKo,
de: appDe,
es: appEs,
fr: appFr,
da: appDa,
ja: appJa,
pl: appPl,
ru: appRu,
ar: appAr,
no: appNo,
br: appBr,
th: appTh,
bs: appBs,
}
const kiloLocales: Record<string, Record<string, string>> = {
en: kiloEn,
zh: kiloZh,
zht: kiloZht,
ko: kiloKo,
de: kiloDe,
es: kiloEs,
fr: kiloFr,
da: kiloDa,
ja: kiloJa,
pl: kiloPl,
ru: kiloRu,
ar: kiloAr,
no: kiloNo,
br: kiloBr,
th: kiloTh,
bs: kiloBs,
}
const uiLocales: Record<string, Record<string, string>> = {
en: uiEn,
zh: uiZh,
zht: uiZht,
ko: uiKo,
de: uiDe,
es: uiEs,
fr: uiFr,
da: uiDa,
ja: uiJa,
pl: uiPl,
ru: uiRu,
ar: uiAr,
no: uiNo,
br: uiBr,
th: uiTh,
bs: uiBs,
}
const cliLocales: Record<string, Record<string, string>> = {
en: cliEn,
zh: cliZh,
zht: cliZht,
de: cliDe,
es: cliEs,
fr: cliFr,
da: cliDa,
ja: cliJa,
ko: cliKo,
pl: cliPl,
ru: cliRu,
ar: cliAr,
no: cliNo,
br: cliBr,
th: cliTh,
bs: cliBs,
}
// Merge webview dictionaries in the same priority order as language.tsx
const webviewKeys = new Set(Object.keys({ ...appEn, ...uiEn, ...kiloEn, ...amEn }))
const cliKeys = new Set(Object.keys(cliEn))
const acKeys = new Set(Object.keys(acEn))
// ── File scanning ───────────────────────────────────────────────────────────
interface Missing {
file: string
line: number
key: string
}
/**
* Regex to match t("key") / t('key') calls, including multiline.
*
* The `s` (dotAll) flag lets `\s*` span newlines so that calls like:
* t(
* "key"
* )
* are captured. Runs against the entire file content, not line-by-line.
*/
const T_CALL = /(?:^|[^a-zA-Z_$])t\(\s*(?:["'])([^"']+)["']/gs
function offsetToLine(content: string, offset: number): number {
let line = 1
for (let i = 0; i < offset && i < content.length; i++) {
if (content[i] === "\n") line++
}
return line
}
function extractKeys(content: string): Array<{ line: number; key: string }> {
const results: Array<{ line: number; key: string }> = []
let match: RegExpExecArray | null
T_CALL.lastIndex = 0
while ((match = T_CALL.exec(content)) !== null) {
const key = match[1]
if (key) results.push({ line: offsetToLine(content, match.index), key })
}
return results
}
async function collectFiles(glob: Glob, dir: string): Promise<string[]> {
const files: string[] = []
for await (const entry of glob.scan({ cwd: dir, absolute: true })) {
files.push(entry)
}
return files
}
// ── Webview files ───────────────────────────────────────────────────────────
async function findWebviewMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.{ts,tsx}")
const dirs = [path.join(ROOT, "webview-ui/src"), path.join(ROOT, "webview-ui/agent-manager")]
const files: string[] = []
for (const dir of dirs) {
files.push(...(await collectFiles(glob, dir)))
}
// Exclude i18n dictionary files themselves and storybook files
const filtered = files.filter((f) => !f.includes("/i18n/") && !f.includes(".stories.") && !f.includes("/stories/"))
const missing: Missing[] = []
for (const file of filtered) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!webviewKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── CLI backend files ───────────────────────────────────────────────────────
async function findCliBackendMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.ts")
const dir = path.join(ROOT, "src/services/cli-backend")
const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/"))
const missing: Missing[] = []
for (const file of files) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!cliKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── Autocomplete files ──────────────────────────────────────────────────────
async function findAutocompleteMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.ts")
const dir = path.join(ROOT, "src/services/autocomplete")
const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/") && !f.includes("/shims/"))
const missing: Missing[] = []
for (const file of files) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!acKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── Locale completeness helpers ─────────────────────────────────────────────
function findMissingLocaleKeys(
en: Record<string, string>,
locales: Record<string, Record<string, string>>,
): Array<{ locale: string; key: string }> {
const base = Object.keys(en)
const results: Array<{ locale: string; key: string }> = []
for (const [locale, dict] of Object.entries(locales)) {
if (locale === "en") continue
const keys = new Set(Object.keys(dict))
for (const key of base) {
if (!keys.has(key)) {
results.push({ locale, key })
}
}
}
return results
}
function formatLocaleReport(items: Array<{ locale: string; key: string }>): string {
return items.map((m) => ` [${m.locale}] "${m.key}"`).join("\n")
}
// ── Tests ───────────────────────────────────────────────────────────────────
function formatReport(missing: Missing[]): string {
return missing.map((m) => ` ${m.file}:${m.line} — "${m.key}"`).join("\n")
}
describe("i18n key validation — no missing translation keys", () => {
it("webview: all t() string literal keys exist in merged dictionaries", async () => {
const missing = await findWebviewMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in any dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("cli-backend: all t() string literal keys exist in cli-backend dictionary", async () => {
const missing = await findCliBackendMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in cli-backend dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("autocomplete: all t() string literal keys exist in autocomplete dictionary", async () => {
const missing = await findAutocompleteMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in autocomplete dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
})
describe("i18n locale completeness — every English key exists in all locales", () => {
it("shared UI: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(uiEn, uiLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing shared UI translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("sidebar app: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(appEn, appLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing sidebar translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("kilo-i18n: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(kiloEn, kiloLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing kilo-i18n translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("cli-backend: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(cliEn, cliLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing cli-backend translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
})
@@ -0,0 +1,61 @@
import { describe, expect, it } from "bun:test"
import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "../../src/image-preview"
describe("parseImage", () => {
it("parses png data urls and preserves a clean extension", () => {
const img = parseImage("data:image/png;base64,aGVsbG8=", "screen")
expect(img).not.toBeNull()
expect(img?.name).toBe("screen.png")
expect(img?.ext).toBe("png")
expect(Buffer.from(img?.data ?? []).toString("utf8")).toBe("hello")
})
it("sanitizes the basename and keeps the existing extension", () => {
const img = parseImage("data:image/jpeg;base64,aGVsbG8=", "../../bad name!!.jpeg")
expect(img).not.toBeNull()
expect(img?.name).toBe("bad-name.jpeg")
expect(img?.ext).toBe("jpg")
})
it("returns null for non-image data urls", () => {
expect(parseImage("data:text/plain;base64,aGVsbG8=", "note.txt")).toBeNull()
})
it("returns null when the header is not base64", () => {
expect(parseImage("data:image/png,hello", "screen.png")).toBeNull()
})
})
describe("buildPreviewPath", () => {
it("writes previews into a dedicated storage folder", () => {
expect(buildPreviewPath("screen.png", 42)).toBe("image-preview/42-screen.png")
})
})
describe("getPreviewDir", () => {
it("returns the preview storage folder", () => {
expect(getPreviewDir()).toBe("image-preview")
})
})
describe("trimEntries", () => {
it("drops the oldest preview paths once the limit is exceeded", () => {
const items = Array.from({ length: 22 }, (_, i) => ({ path: `${String(i).padStart(2, "0")}-screen.png` }))
expect(trimEntries(items)).toEqual(["00-screen.png", "01-screen.png"])
})
it("keeps all preview paths when below the limit", () => {
expect(trimEntries([{ path: "01-screen.png" }])).toEqual([])
})
})
describe("getPreviewCommand", () => {
it("targets the built-in preview editor with explicit sizing", () => {
const uri = { toString: () => "file:///tmp/screen.png" }
expect(getPreviewCommand(uri)).toEqual(["imagePreview.previewEditor", { resource: uri, size: "contain" }])
})
})
@@ -284,6 +284,7 @@ describe("mapSSEEventToWebviewMessage", () => {
expect(msg.permission.args).toEqual({ path: "/foo" })
expect(msg.permission.message).toBe("Permission required: read_file")
expect(msg.permission.patterns).toEqual(["**/*.ts"])
expect(msg.permission.always).toEqual([])
}
})
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de6be977bd3805fdbbbb1fdaf3498201c1097adea26ebe460c56b681d1828e39
size 9457
oid sha256:c5ada07e94ff10490732ba4b27d8e204102816aa00e472324a31ee9f8c7e8484
size 9470
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3164d510232a0457cf935a3032f736825486f133e89994e544c678ccc7e6849a
size 30652
oid sha256:0bbec9564c9fa46fce155ada8e176641e540c912461102b7b424d317b3283c38
size 31033
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc40d7bbc5f3eaee3b48b4f90046a10e54db3288e71214eec190e4b0fa61cc16
size 22777
oid sha256:48ebccc3ab71403efa74803f140b17d97a000989b64dd42c65d8f9774d059cb8
size 23139
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3da4a2d29601e45eb4b5fe007a62a203c2b5151b2d74437d22630d2ddc8582a6
size 28723
oid sha256:371621a81d9c42d1dc327492fb2e3d2516857dfa189adf3c924ac6951d33e910
size 29073

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