-
+
-
- {t("settings.experimental.websearch.title")}
+
+ {t("settings.webTools.webSearch.title")}
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
index f86fe517b07..02fc76303d4 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
@@ -1203,7 +1203,13 @@ export const dict = {
"settings.section.configuration": "الإعدادات",
"settings.agentBehaviour.title": "سلوك الوكيل",
"settings.autoApprove.title": "الموافقة التلقائية",
- "settings.browser.title": "المتصفح",
+ "settings.webTools.title": "أدوات الويب",
+ "settings.webTools.description": "اضبط البحث على الويب وأتمتة المتصفح. تتصل طلبات البحث مباشرةً بـ Exa أو Parallel.",
+ "settings.webTools.webSearch.enable": "تمكين لجميع المزوّدين",
+ "settings.webTools.browserAutomation": "أتمتة المتصفح",
+ "settings.webTools.webSearch.title": "البحث على الويب",
+ "settings.webTools.webSearch.description":
+ "اجعل البحث على الويب متاحًا لنماذج جميع المزوّدين. تتصل عمليات البحث مباشرةً بـ Exa أو Parallel.",
"settings.checkpoints.title": "نقاط التحقق",
"settings.display.title": "العرض",
"settings.autocomplete.title": "الإكمال التلقائي",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
index 35a1450ea1f..902114dd85c 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
@@ -1224,7 +1224,14 @@ export const dict = {
"settings.section.configuration": "Configuração",
"settings.agentBehaviour.title": "Comportamento do Agente",
"settings.autoApprove.title": "Aprovação Automática",
- "settings.browser.title": "Navegador",
+ "settings.webTools.title": "Ferramentas da Web",
+ "settings.webTools.description":
+ "Configure a pesquisa na web e a automação do navegador. As solicitações de pesquisa se conectam diretamente ao Exa ou Parallel.",
+ "settings.webTools.webSearch.enable": "Ativar para todos os provedores",
+ "settings.webTools.browserAutomation": "Automação do navegador",
+ "settings.webTools.webSearch.title": "Pesquisa na Web",
+ "settings.webTools.webSearch.description":
+ "Disponibilize a pesquisa na web para modelos de todos os provedores. As pesquisas se conectam diretamente ao Exa ou Parallel.",
"settings.checkpoints.title": "Pontos de Verificação",
"settings.display.title": "Exibição",
"settings.autocomplete.title": "Autocompletar",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
index c19e73262cd..dbb2e7b9230 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
@@ -1269,7 +1269,14 @@ export const dict = {
"settings.section.configuration": "Konfiguracija",
"settings.agentBehaviour.title": "Ponašanje agenta",
"settings.autoApprove.title": "Automatsko odobravanje",
- "settings.browser.title": "Preglednik",
+ "settings.webTools.title": "Web alati",
+ "settings.webTools.description":
+ "Konfigurišite web pretragu i automatizaciju preglednika. Zahtjevi za pretragu povezuju se direktno s Exa ili Parallel.",
+ "settings.webTools.webSearch.enable": "Omogući za sve pružaoce",
+ "settings.webTools.browserAutomation": "Automatizacija preglednika",
+ "settings.webTools.webSearch.title": "Web pretraga",
+ "settings.webTools.webSearch.description":
+ "Omogućite web pretragu modelima svih pružalaca. Pretrage se povezuju direktno s Exa ili Parallel.",
"settings.checkpoints.title": "Kontrolne tačke",
"settings.display.title": "Prikaz",
"settings.autocomplete.title": "Automatsko dovršavanje",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
index 8e9a593dd37..028595f8599 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
@@ -1262,7 +1262,14 @@ export const dict = {
"settings.section.configuration": "Konfiguration",
"settings.agentBehaviour.title": "Agentadfærd",
"settings.autoApprove.title": "Automatisk godkendelse",
- "settings.browser.title": "Browser",
+ "settings.webTools.title": "Webværktøjer",
+ "settings.webTools.description":
+ "Konfigurer websøgning og browserautomatisering. Søgeanmodninger sendes direkte til Exa eller Parallel.",
+ "settings.webTools.webSearch.enable": "Aktivér for alle udbydere",
+ "settings.webTools.browserAutomation": "Browserautomatisering",
+ "settings.webTools.webSearch.title": "Websøgning",
+ "settings.webTools.webSearch.description":
+ "Gør websøgning tilgængelig for modeller fra alle udbydere. Søgninger sendes direkte til Exa eller Parallel.",
"settings.checkpoints.title": "Kontrolpunkter",
"settings.display.title": "Visning",
"settings.autocomplete.title": "Autofuldførelse",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
index 380e7d3db41..4adcf7b72f8 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
@@ -1283,7 +1283,14 @@ export const dict = {
"settings.section.configuration": "Konfiguration",
"settings.agentBehaviour.title": "Agentenverhalten",
"settings.autoApprove.title": "Automatisch genehmigen",
- "settings.browser.title": "Browser",
+ "settings.webTools.title": "Web-Tools",
+ "settings.webTools.description":
+ "Konfigurieren Sie Websuche und Browserautomatisierung. Suchanfragen werden direkt an Exa oder Parallel gesendet.",
+ "settings.webTools.webSearch.enable": "Für alle Anbieter aktivieren",
+ "settings.webTools.browserAutomation": "Browserautomatisierung",
+ "settings.webTools.webSearch.title": "Websuche",
+ "settings.webTools.webSearch.description":
+ "Machen Sie die Websuche für Modelle aller Anbieter verfügbar. Suchanfragen werden direkt an Exa oder Parallel gesendet.",
"settings.checkpoints.title": "Prüfpunkte",
"settings.display.title": "Anzeige",
"settings.autocomplete.title": "Autovervollständigung",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
index abe0350075a..e926699acc2 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
@@ -1176,8 +1176,14 @@ export const dict = {
"settings.section.configuration": "Configuration",
"settings.agentBehaviour.title": "Agent Behaviour",
"settings.autoApprove.title": "Auto-Approve",
- "settings.browser.title": "Browser",
"settings.webTools.title": "Web Tools",
+ "settings.webTools.description":
+ "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.",
+ "settings.webTools.webSearch.enable": "Enable for All Providers",
+ "settings.webTools.browserAutomation": "Browser Automation",
+ "settings.webTools.webSearch.title": "Web Search",
+ "settings.webTools.webSearch.description":
+ "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.",
"settings.checkpoints.title": "Checkpoints",
"settings.display.title": "Display",
"settings.autocomplete.title": "Autocomplete",
@@ -1350,10 +1356,6 @@ export const dict = {
"settings.browser.description":
"When enabled, the AI agent can interact with web pages — navigating, clicking, typing, and taking screenshots. A Chrome window will open so you can watch the agent work.",
- "settings.webTools.description":
- "Configure web search and browser automation. Search requests connect directly to Exa or Parallel.",
- "settings.webTools.websearchEnable": "Enable for All Providers",
- "settings.webTools.browserAutomation": "Browser Automation",
"settings.browser.enable.title": "Enable Browser Automation",
"settings.browser.enable.description": "Register the Playwright MCP server with the CLI backend.",
"settings.browser.systemChrome.title": "Use System Chrome",
@@ -1414,9 +1416,6 @@ export const dict = {
"settings.experimental.lsp.description": "Enable language server protocol integration",
"settings.experimental.batch.title": "Batch Tool",
"settings.experimental.batch.description": "Enable batching of multiple tool calls",
- "settings.experimental.websearch.title": "Web Search",
- "settings.experimental.websearch.description":
- "Make web search available to models from all providers. Searches connect directly to Exa or Parallel.",
"settings.experimental.codebaseSearch.title": "Codebase Search",
"settings.experimental.codebaseSearch.description": "Enable AI-powered natural language search across your codebase",
"settings.experimental.imageGeneration.title": "Image Generation",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
index e1cd4296143..d66f4bd86f6 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
@@ -1276,7 +1276,14 @@ export const dict = {
"settings.section.configuration": "Configuración",
"settings.agentBehaviour.title": "Comportamiento del agente",
"settings.autoApprove.title": "Aprobación automática",
- "settings.browser.title": "Navegador",
+ "settings.webTools.title": "Herramientas web",
+ "settings.webTools.description":
+ "Configura la búsqueda web y la automatización del navegador. Las solicitudes de búsqueda se conectan directamente a Exa o Parallel.",
+ "settings.webTools.webSearch.enable": "Habilitar para todos los proveedores",
+ "settings.webTools.browserAutomation": "Automatización del navegador",
+ "settings.webTools.webSearch.title": "Búsqueda web",
+ "settings.webTools.webSearch.description":
+ "Permite que los modelos de todos los proveedores usen la búsqueda web. Las búsquedas se conectan directamente a Exa o Parallel.",
"settings.checkpoints.title": "Puntos de control",
"settings.display.title": "Pantalla",
"settings.autocomplete.title": "Autocompletado",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
index 71055c0d653..1ed7c73e8bd 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
@@ -1287,7 +1287,14 @@ export const dict = {
"settings.section.configuration": "Configuration",
"settings.agentBehaviour.title": "Comportement de l'agent",
"settings.autoApprove.title": "Approbation automatique",
- "settings.browser.title": "Navigateur",
+ "settings.webTools.title": "Outils web",
+ "settings.webTools.description":
+ "Configurez la recherche web et l’automatisation du navigateur. Les requêtes de recherche se connectent directement à Exa ou Parallel.",
+ "settings.webTools.webSearch.enable": "Activer pour tous les fournisseurs",
+ "settings.webTools.browserAutomation": "Automatisation du navigateur",
+ "settings.webTools.webSearch.title": "Recherche web",
+ "settings.webTools.webSearch.description":
+ "Rendez la recherche web disponible pour les modèles de tous les fournisseurs. Les recherches se connectent directement à Exa ou Parallel.",
"settings.checkpoints.title": "Points de contrôle",
"settings.display.title": "Affichage",
"settings.autocomplete.title": "Autocomplétion",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts
index 034276bb9c4..b0c9a5f7038 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts
@@ -1023,7 +1023,14 @@ export const dict = {
"settings.section.configuration": "Configurazione",
"settings.agentBehaviour.title": "Comportamento agente",
"settings.autoApprove.title": "Approvazione automatica",
- "settings.browser.title": "Browser",
+ "settings.webTools.title": "Strumenti web",
+ "settings.webTools.description":
+ "Configura la ricerca web e l'automazione del browser. Le richieste di ricerca si connettono direttamente a Exa o Parallel.",
+ "settings.webTools.webSearch.enable": "Abilita per tutti i provider",
+ "settings.webTools.browserAutomation": "Automazione del browser",
+ "settings.webTools.webSearch.title": "Ricerca web",
+ "settings.webTools.webSearch.description":
+ "Rendi disponibile la ricerca web ai modelli di tutti i provider. Le ricerche si connettono direttamente a Exa o Parallel.",
"settings.checkpoints.title": "Checkpoint",
"settings.display.title": "Visualizzazione",
"settings.autocomplete.title": "Autocompletamento",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
index 2c6bd6c9e3c..fd60365ca9f 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
@@ -1257,7 +1257,14 @@ export const dict = {
"settings.section.configuration": "設定",
"settings.agentBehaviour.title": "エージェントの動作",
"settings.autoApprove.title": "自動承認",
- "settings.browser.title": "ブラウザ",
+ "settings.webTools.title": "ウェブツール",
+ "settings.webTools.description":
+ "ウェブ検索とブラウザ自動化を設定します。検索リクエストは Exa または Parallel に直接接続されます。",
+ "settings.webTools.webSearch.enable": "すべてのプロバイダーで有効化",
+ "settings.webTools.browserAutomation": "ブラウザ自動化",
+ "settings.webTools.webSearch.title": "ウェブ検索",
+ "settings.webTools.webSearch.description":
+ "すべてのプロバイダーのモデルでウェブ検索を利用できるようにします。検索は Exa または Parallel に直接接続されます。",
"settings.checkpoints.title": "チェックポイント",
"settings.display.title": "表示",
"settings.autocomplete.title": "オートコンプリート",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
index 990edeb5eb3..9dd03144c25 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
@@ -1210,7 +1210,14 @@ export const dict = {
"settings.section.configuration": "구성",
"settings.agentBehaviour.title": "에이전트 동작",
"settings.autoApprove.title": "자동 승인",
- "settings.browser.title": "브라우저",
+ "settings.webTools.title": "웹 도구",
+ "settings.webTools.description":
+ "웹 검색 및 브라우저 자동화를 구성합니다. 검색 요청은 Exa 또는 Parallel에 직접 연결됩니다.",
+ "settings.webTools.webSearch.enable": "모든 제공업체에 사용",
+ "settings.webTools.browserAutomation": "브라우저 자동화",
+ "settings.webTools.webSearch.title": "웹 검색",
+ "settings.webTools.webSearch.description":
+ "모든 제공업체의 모델에서 웹 검색을 사용할 수 있도록 합니다. 검색은 Exa 또는 Parallel에 직접 연결됩니다.",
"settings.checkpoints.title": "체크포인트",
"settings.display.title": "디스플레이",
"settings.autocomplete.title": "자동 완성",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts
index 75d20e4f178..8158160380a 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts
@@ -1218,7 +1218,14 @@ export const dict = {
"settings.section.configuration": "Configuratie",
"settings.agentBehaviour.title": "Agent Gedrag",
"settings.autoApprove.title": "Automatisch Goedkeuren",
- "settings.browser.title": "Browser",
+ "settings.webTools.title": "Webtools",
+ "settings.webTools.description":
+ "Configureer zoeken op internet en browserautomatisering. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.",
+ "settings.webTools.webSearch.enable": "Inschakelen voor alle providers",
+ "settings.webTools.browserAutomation": "Browserautomatisering",
+ "settings.webTools.webSearch.title": "Zoeken op internet",
+ "settings.webTools.webSearch.description":
+ "Maak zoeken op internet beschikbaar voor modellen van alle providers. Zoekopdrachten maken rechtstreeks verbinding met Exa of Parallel.",
"settings.checkpoints.title": "Controlepunten",
"settings.display.title": "Weergave",
"settings.autocomplete.title": "Automatisch Aanvullen",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
index 68c4af800b8..5e8b9faf1f4 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
@@ -1222,7 +1222,14 @@ export const dict = {
"settings.section.configuration": "Konfigurasjon",
"settings.agentBehaviour.title": "Agentoppførsel",
"settings.autoApprove.title": "Automatisk godkjenning",
- "settings.browser.title": "Nettleser",
+ "settings.webTools.title": "Nettverktøy",
+ "settings.webTools.description":
+ "Konfigurer nettsøk og nettleserautomatisering. Søk sendes direkte til Exa eller Parallel.",
+ "settings.webTools.webSearch.enable": "Aktiver for alle leverandører",
+ "settings.webTools.browserAutomation": "Nettleserautomatisering",
+ "settings.webTools.webSearch.title": "Nettsøk",
+ "settings.webTools.webSearch.description":
+ "Gjør nettsøk tilgjengelig for modeller fra alle leverandører. Søk sendes direkte til Exa eller Parallel.",
"settings.checkpoints.title": "Kontrollpunkter",
"settings.display.title": "Visning",
"settings.autocomplete.title": "Autofullfør",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
index f7238a444cb..aa920ac5db3 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
@@ -1221,7 +1221,14 @@ export const dict = {
"settings.section.configuration": "Konfiguracja",
"settings.agentBehaviour.title": "Zachowanie agenta",
"settings.autoApprove.title": "Automatyczne zatwierdzanie",
- "settings.browser.title": "Przeglądarka",
+ "settings.webTools.title": "Narzędzia internetowe",
+ "settings.webTools.description":
+ "Skonfiguruj wyszukiwanie w sieci i automatyzację przeglądarki. Żądania wyszukiwania łączą się bezpośrednio z Exa lub Parallel.",
+ "settings.webTools.webSearch.enable": "Włącz dla wszystkich dostawców",
+ "settings.webTools.browserAutomation": "Automatyzacja przeglądarki",
+ "settings.webTools.webSearch.title": "Wyszukiwanie w sieci",
+ "settings.webTools.webSearch.description":
+ "Udostępnij wyszukiwanie w sieci modelom wszystkich dostawców. Wyszukiwania łączą się bezpośrednio z Exa lub Parallel.",
"settings.checkpoints.title": "Punkty kontrolne",
"settings.display.title": "Wyświetlanie",
"settings.autocomplete.title": "Autouzupełnianie",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
index 47a24933c3a..68027826b86 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
@@ -1268,7 +1268,14 @@ export const dict = {
"settings.section.configuration": "Конфигурация",
"settings.agentBehaviour.title": "Поведение агента",
"settings.autoApprove.title": "Автоодобрение",
- "settings.browser.title": "Браузер",
+ "settings.webTools.title": "Веб-инструменты",
+ "settings.webTools.description":
+ "Настройте веб-поиск и автоматизацию браузера. Поисковые запросы отправляются напрямую в Exa или Parallel.",
+ "settings.webTools.webSearch.enable": "Включить для всех провайдеров",
+ "settings.webTools.browserAutomation": "Автоматизация браузера",
+ "settings.webTools.webSearch.title": "Веб-поиск",
+ "settings.webTools.webSearch.description":
+ "Сделайте веб-поиск доступным для моделей всех провайдеров. Поисковые запросы отправляются напрямую в Exa или Parallel.",
"settings.checkpoints.title": "Контрольные точки",
"settings.display.title": "Отображение",
"settings.autocomplete.title": "Автодополнение",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
index cac0eea7a9b..c029e495b63 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
@@ -1250,7 +1250,14 @@ export const dict = {
"settings.section.configuration": "การกำหนดค่า",
"settings.agentBehaviour.title": "พฤติกรรมของเอเจนต์",
"settings.autoApprove.title": "อนุมัติอัตโนมัติ",
- "settings.browser.title": "เบราว์เซอร์",
+ "settings.webTools.title": "เครื่องมือเว็บ",
+ "settings.webTools.description":
+ "กำหนดค่าการค้นหาเว็บและระบบอัตโนมัติของเบราว์เซอร์ คำขอค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel",
+ "settings.webTools.webSearch.enable": "เปิดใช้สำหรับผู้ให้บริการทั้งหมด",
+ "settings.webTools.browserAutomation": "ระบบอัตโนมัติของเบราว์เซอร์",
+ "settings.webTools.webSearch.title": "ค้นหาเว็บ",
+ "settings.webTools.webSearch.description":
+ "ทำให้โมเดลจากผู้ให้บริการทั้งหมดใช้การค้นหาเว็บได้ การค้นหาจะเชื่อมต่อโดยตรงกับ Exa หรือ Parallel",
"settings.checkpoints.title": "จุดตรวจสอบ",
"settings.display.title": "การแสดงผล",
"settings.autocomplete.title": "เติมข้อความอัตโนมัติ",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts
index e799d071417..a43c79880d2 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts
@@ -1215,7 +1215,14 @@ export const dict = {
"settings.section.configuration": "Yapılandırma",
"settings.agentBehaviour.title": "Ajan Davranışı",
"settings.autoApprove.title": "Otomatik Onay",
- "settings.browser.title": "Tarayıcı",
+ "settings.webTools.title": "Web Araçları",
+ "settings.webTools.description":
+ "Web aramasını ve tarayıcı otomasyonunu yapılandırın. Arama istekleri doğrudan Exa veya Parallel'e bağlanır.",
+ "settings.webTools.webSearch.enable": "Tüm Sağlayıcılar İçin Etkinleştir",
+ "settings.webTools.browserAutomation": "Tarayıcı Otomasyonu",
+ "settings.webTools.webSearch.title": "Web Araması",
+ "settings.webTools.webSearch.description":
+ "Web aramasını tüm sağlayıcıların modelleri için kullanılabilir hale getirin. Aramalar doğrudan Exa veya Parallel'e bağlanır.",
"settings.checkpoints.title": "Kontrol Noktaları",
"settings.display.title": "Görünüm",
"settings.autocomplete.title": "Otomatik Tamamlama",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts
index dfc15c248db..75783bc9e42 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts
@@ -1213,7 +1213,14 @@ export const dict = {
"settings.section.configuration": "Конфігурація",
"settings.agentBehaviour.title": "Поведінка агента",
"settings.autoApprove.title": "Автоматичне схвалення",
- "settings.browser.title": "Браузер",
+ "settings.webTools.title": "Вебінструменти",
+ "settings.webTools.description":
+ "Налаштуйте вебпошук і автоматизацію браузера. Пошукові запити надсилаються безпосередньо до Exa або Parallel.",
+ "settings.webTools.webSearch.enable": "Увімкнути для всіх постачальників",
+ "settings.webTools.browserAutomation": "Автоматизація браузера",
+ "settings.webTools.webSearch.title": "Вебпошук",
+ "settings.webTools.webSearch.description":
+ "Зробіть вебпошук доступним для моделей усіх постачальників. Пошукові запити надсилаються безпосередньо до Exa або Parallel.",
"settings.checkpoints.title": "Контрольні точки",
"settings.display.title": "Відображення",
"settings.autocomplete.title": "Автодоповнення",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
index 8432bbee901..9e1dd057295 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
@@ -1229,7 +1229,12 @@ export const dict = {
"settings.section.configuration": "配置",
"settings.agentBehaviour.title": "智能体行为",
"settings.autoApprove.title": "自动审批",
- "settings.browser.title": "浏览器",
+ "settings.webTools.title": "网络工具",
+ "settings.webTools.description": "配置网页搜索和浏览器自动化。搜索请求会直接连接到 Exa 或 Parallel。",
+ "settings.webTools.webSearch.enable": "为所有提供商启用",
+ "settings.webTools.browserAutomation": "浏览器自动化",
+ "settings.webTools.webSearch.title": "网页搜索",
+ "settings.webTools.webSearch.description": "让所有提供商的模型都可使用网页搜索。搜索会直接连接到 Exa 或 Parallel。",
"settings.checkpoints.title": "检查点",
"settings.display.title": "显示",
"settings.autocomplete.title": "自动补全",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
index b69bb7054c1..aed2a971794 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
@@ -1190,7 +1190,12 @@ export const dict = {
"settings.section.configuration": "設定",
"settings.agentBehaviour.title": "Agent 行為",
"settings.autoApprove.title": "自動核准",
- "settings.browser.title": "瀏覽器",
+ "settings.webTools.title": "網路工具",
+ "settings.webTools.description": "設定網頁搜尋和瀏覽器自動化。搜尋請求會直接連線至 Exa 或 Parallel。",
+ "settings.webTools.webSearch.enable": "為所有供應商啟用",
+ "settings.webTools.browserAutomation": "瀏覽器自動化",
+ "settings.webTools.webSearch.title": "網頁搜尋",
+ "settings.webTools.webSearch.description": "讓所有供應商的模型都可使用網頁搜尋。搜尋會直接連線至 Exa 或 Parallel。",
"settings.checkpoints.title": "檢查點",
"settings.display.title": "顯示",
"settings.autocomplete.title": "自動完成",
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts
index 0dc2d05dc71..89dcc0af832 100644
--- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts
+++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts
@@ -39,7 +39,6 @@ export interface WatcherConfig {
export interface ExperimentalConfig {
batch_tool?: boolean
- websearch?: boolean
codebase_search?: boolean
image_generation?: boolean
image_generation_model?: string
@@ -155,6 +154,7 @@ export interface Config {
compaction?: CompactionConfig
commit_message?: CommitMessageConfig
tools?: Record
+ web_search?: boolean
auto_collapse_reasoning?: boolean
experimental?: ExperimentalConfig
sandbox?: SandboxConfig
diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts
index b0cd42f88c3..ec806bf053f 100644
--- a/packages/opencode/src/kilocode/config/overlay.ts
+++ b/packages/opencode/src/kilocode/config/overlay.ts
@@ -88,7 +88,7 @@ export namespace KilocodeConfigOverlay {
["disabled_providers"],
["watcher", "ignore"],
["instructions"],
- ["experimental", "websearch"],
+ ["web_search"],
["indexing", "enabled"],
["indexing", "provider"],
["indexing", "model"],
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index f8eb66469ab..4df567dd6bf 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -377,7 +377,7 @@ export const layer: Layer.Layer<
const filtered = (yield* all()).filter((tool) => {
if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change
if (tool.id === WebSearchTool.id) {
- if (cfg.experimental?.websearch === true) return true // kilocode_change
+ if (cfg.web_search === true) return true // kilocode_change
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
}
diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts
index 50529c043ff..51902e15142 100644
--- a/packages/opencode/test/kilocode/config/config.test.ts
+++ b/packages/opencode/test/kilocode/config/config.test.ts
@@ -151,13 +151,15 @@ describe("global config updates", () => {
})
})
-describe("kilocode indexing config", () => {
+describe("kilocode web search config", () => {
test("accepts the websearch availability setting", () => {
- const config = Schema.decodeUnknownSync(Config.Info)({ experimental: { websearch: true } })
+ const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true })
- expect(config.experimental?.websearch).toBe(true)
+ expect(config.web_search).toBe(true)
})
+})
+describe("kilocode indexing config", () => {
test("ignores retired semantic indexing flags in existing configs", async () => {
await using tmp = await tmpdir({ git: true })
await writeConfig(tmp.path, {
diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts
index e8a55f9ac7f..a21deb1e5db 100644
--- a/packages/opencode/test/kilocode/server/config-overlay.test.ts
+++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts
@@ -204,17 +204,17 @@ describe("config overlay routes", () => {
test.serial("resolves and reverts project websearch overrides", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
- await setGlobal(global.path, { experimental: { websearch: true } })
+ await setGlobal(global.path, { web_search: true })
await json(
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
- body: JSON.stringify({ scope: "project", set: { experimental: { websearch: false } } }),
+ body: JSON.stringify({ scope: "project", set: { web_search: false } }),
}),
)
const overridden = await json(await req(project.path, "/config/overlay?scope=project"))
- expect(overridden.fields["experimental.websearch"]).toMatchObject({
+ expect(overridden.fields.web_search).toMatchObject({
source: "project",
inherited: false,
overridden: true,
@@ -225,11 +225,11 @@ describe("config overlay routes", () => {
await req(project.path, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
- body: JSON.stringify({ scope: "project", unset: [["experimental", "websearch"]] }),
+ body: JSON.stringify({ scope: "project", unset: [["web_search"]] }),
}),
)
const inherited = await json(await req(project.path, "/config/overlay?scope=project"))
- expect(inherited.fields["experimental.websearch"]).toMatchObject({
+ expect(inherited.fields.web_search).toMatchObject({
source: "global",
inherited: true,
overridden: false,
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index 393bea6f25e..97e4e6c51c1 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -52,12 +52,13 @@ const configLayer = TestConfig.layer({
type RegistryLayerOptions = {
flags?: Partial
plugin?: Layer.Layer
+ config?: Parameters[0] // kilocode_change
}
const registryLayer = (opts: RegistryLayerOptions = {}) =>
ToolRegistry.layer
.pipe(
- Layer.provide(configLayer),
+ Layer.provide(opts.config ? TestConfig.layer(opts.config) : configLayer), // kilocode_change
Layer.provide(opts.plugin ?? Plugin.defaultLayer),
Layer.provide(Question.defaultLayer),
Layer.provide(Todo.defaultLayer),
@@ -118,6 +119,21 @@ const withBrokenPlugin = testEffect(
Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer),
)
// kilocode_change start
+const websearch = testEffect(
+ Layer.mergeAll(
+ registryLayer({
+ config: {
+ get: () =>
+ Effect.succeed({
+ web_search: true,
+ provider: { openai: { options: { apiKey: "test-openai-key" } } },
+ }),
+ },
+ }),
+ node,
+ Agent.defaultLayer,
+ ),
+)
const sandboxed = testEffect(
Layer.mergeAll(registryLayer({ flags: { experimentalLspTool: true } }), node, Agent.defaultLayer),
)
@@ -139,6 +155,38 @@ function sandboxProfile(): Profile {
describe("tool.registry", () => {
// kilocode_change start
+ it.instance("hides websearch for a third-party provider by default", () =>
+ Effect.gen(function* () {
+ const registry = yield* ToolRegistry.Service
+ const agent = yield* Agent.Service
+ const build = yield* agent.get("build")
+ if (!build) return yield* Effect.die(new Error("build agent not found"))
+ const tools = yield* registry.tools({
+ providerID: ProviderV2.ID.openai,
+ modelID: ModelV2.ID.make("test"),
+ agent: build,
+ })
+
+ expect(tools.map((tool) => tool.id)).not.toContain("websearch")
+ }),
+ )
+
+ websearch.instance("shows websearch for a configured third-party provider when enabled", () =>
+ Effect.gen(function* () {
+ const registry = yield* ToolRegistry.Service
+ const agent = yield* Agent.Service
+ const build = yield* agent.get("build")
+ if (!build) return yield* Effect.die(new Error("build agent not found"))
+ const tools = yield* registry.tools({
+ providerID: ProviderV2.ID.openai,
+ modelID: ModelV2.ID.make("test"),
+ agent: build,
+ })
+
+ expect(tools.map((tool) => tool.id)).toContain("websearch")
+ }),
+ )
+
sandboxed.instance("preserves built-in network classification through production tool definition processing", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 0bd15820a76..5f9963def06 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1673,6 +1673,7 @@ export type Config = {
tools?: {
[key: string]: boolean
}
+ web_search?: boolean
attachment?: AttachmentConfig
enterprise?: {
url?: string
@@ -1699,7 +1700,6 @@ export type Config = {
disable_paste_summary?: boolean
batch_tool?: boolean
codebase_search?: boolean
- websearch?: boolean
image_generation?: boolean
image_generation_model?: string
agent_requirements?: boolean
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 7ea6d2387db..1451c3a6b3f 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -26905,6 +26905,9 @@
"type": "boolean"
}
},
+ "web_search": {
+ "type": "boolean"
+ },
"attachment": {
"$ref": "#/components/schemas/AttachmentConfig"
},
@@ -27000,9 +27003,6 @@
"codebase_search": {
"type": "boolean"
},
- "websearch": {
- "type": "boolean"
- },
"image_generation": {
"type": "boolean"
},
From 25dcfb6cff23abe840f3a728ebe48deef6af9355 Mon Sep 17 00:00:00 2001
From: Kelly Sun
Date: Thu, 23 Jul 2026 14:28:32 -0400
Subject: [PATCH 03/71] docs: add Mixlayer provider page
Adds an AI-providers docs page for Mixlayer (OpenAI-compatible inference
for open models like GLM and Qwen) and a nav entry, documenting the
OpenAI-Compatible setup for both VS Code and the CLI.
---
packages/kilo-docs/lib/nav/ai-providers.ts | 1 +
.../kilo-docs/pages/ai-providers/mixlayer.md | 97 +++++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 packages/kilo-docs/pages/ai-providers/mixlayer.md
diff --git a/packages/kilo-docs/lib/nav/ai-providers.ts b/packages/kilo-docs/lib/nav/ai-providers.ts
index f604826c7cb..1a2dfe9ae35 100644
--- a/packages/kilo-docs/lib/nav/ai-providers.ts
+++ b/packages/kilo-docs/lib/nav/ai-providers.ts
@@ -46,6 +46,7 @@ export const AiProvidersNav: NavSection[] = [
{ href: "/ai-providers/groq", children: "Groq" },
{ href: "/ai-providers/cerebras", children: "Cerebras" },
{ href: "/ai-providers/fireworks", children: "Fireworks AI" },
+ { href: "/ai-providers/mixlayer", children: "Mixlayer" },
],
},
{
diff --git a/packages/kilo-docs/pages/ai-providers/mixlayer.md b/packages/kilo-docs/pages/ai-providers/mixlayer.md
new file mode 100644
index 00000000000..ac0221315a6
--- /dev/null
+++ b/packages/kilo-docs/pages/ai-providers/mixlayer.md
@@ -0,0 +1,97 @@
+---
+title: "Using Mixlayer with Kilo Code | Fast Open-Model Inference"
+description: "Run open models like GLM and Qwen on Mixlayer's OpenAI-compatible API in Kilo Code. Setup guide for VS Code and the CLI."
+---
+
+# Using Mixlayer With Kilo Code
+
+Mixlayer is an inference platform for open models such as GLM and Qwen, with a serving stack built from scratch by core contributors to Candle. It exposes an OpenAI-compatible API, so you can use it in Kilo Code through the **OpenAI Compatible** provider.
+
+**Website:** [https://mixlayer.com/](https://mixlayer.com/)
+
+## Getting an API Key
+
+1. **Sign Up/Sign In:** Go to [Mixlayer](https://mixlayer.com/) and create an account or sign in.
+2. **Navigate to API Keys:** Open the [Mixlayer console](https://console.mixlayer.com/) and go to the API Keys page.
+3. **Create a Key:** Click **New Key**, give it a descriptive name (e.g., "Kilo Code"), and copy it. You will not be able to view it again.
+
+## Configuration in Kilo Code
+
+Mixlayer's API is OpenAI-compatible, with the base URL `https://models.mixlayer.ai/v1`. Configure it through Kilo Code's **OpenAI Compatible** provider.
+
+{% tabs %}
+{% tab label="VSCode" %}
+
+1. Open **Settings** (gear icon) and go to the **Providers** tab.
+2. Scroll to the bottom and click **Custom provider**.
+3. Fill in the dialog:
+ - **Provider ID** — `mixlayer`
+ - **Display name** — `Mixlayer`
+ - **Provider API** — **OpenAI Compatible**
+ - **Base URL** — `https://models.mixlayer.ai/v1`
+ - **API key** — your Mixlayer API key
+4. Kilo Code auto-fetches the available models from Mixlayer's `/v1/models` endpoint, so you can pick a model directly from the list. Click **Submit** to save.
+
+{% /tab %}
+{% tab label="CLI" %}
+
+Set the API key as an environment variable and define an OpenAI-compatible provider in your `kilo.json` config file (`~/.config/kilo/kilo.json` or `./kilo.json`):
+
+**Environment variable:**
+
+```bash
+export MIXLAYER_API_KEY="your-api-key"
+```
+
+**Config file:**
+
+```jsonc
+{
+ "provider": {
+ "mixlayer": {
+ "npm": "@ai-sdk/openai-compatible",
+ "env": ["MIXLAYER_API_KEY"],
+ "options": {
+ "baseURL": "https://models.mixlayer.ai/v1",
+ },
+ "models": {
+ "z-ai/glm-5.2": {
+ "name": "GLM-5.2",
+ "limit": { "context": 262144, "output": 262144 },
+ },
+ "qwen/qwen3.5-397b-a17b": {
+ "name": "Qwen3.5 397B A17B",
+ "limit": { "context": 131072, "output": 131072 },
+ },
+ },
+ },
+ },
+}
+```
+
+Then set your default model using the `provider-id/model-id` format:
+
+```jsonc
+{
+ "model": "mixlayer/z-ai/glm-5.2",
+}
+```
+
+{% /tab %}
+{% /tabs %}
+
+## Models
+
+Mixlayer serves open models including:
+
+- `z-ai/glm-5.2` — 256K context
+- `qwen/qwen3.5-397b-a17b` and the Qwen 3.5 / 3.6 line (vision-capable)
+- `moonshotai/kimi-k2.7-code`
+
+Tool calling and reasoning are supported across the model line. See the [Mixlayer docs](https://docs.mixlayer.com) for the full, current model list and supported parameters.
+
+## Tips and Notes
+
+- **Model list:** Kilo Code auto-detects available models from Mixlayer's `/v1/models` endpoint, so the picker stays current with your account.
+- **Pricing:** See the [Mixlayer console](https://console.mixlayer.com/) for current per-model pricing.
+- **Reasoning:** Qwen models support a thinking mode; reasoning tokens count against the output budget, so allow enough `limit.output` when reasoning is enabled.
From 34e787e5ac039b0d070ed186ee5979aba38c8576 Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Mon, 27 Jul 2026 11:48:09 -0400
Subject: [PATCH 04/71] fix: enable web search by default
---
packages/core/src/v1/config/config.ts | 3 +-
.../src/routes/config/ToolsRoute.tsx | 2 +-
.../src/components/settings/BrowserTab.tsx | 2 +-
packages/opencode/src/tool/registry.ts | 2 +-
.../test/kilocode/config/config.test.ts | 6 +--
packages/opencode/test/tool/registry.test.ts | 50 ++++++++++++-------
6 files changed, 40 insertions(+), 25 deletions(-)
diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts
index bd9baab123d..0ef9d1ffc71 100644
--- a/packages/core/src/v1/config/config.ts
+++ b/packages/core/src/v1/config/config.ts
@@ -227,7 +227,8 @@ export const Info = Schema.Struct({
permission: Schema.optional(ConfigPermissionV1.Info),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
web_search: Schema.optional(Schema.Boolean).annotate({
- description: "Make web search available to models from all providers",
+ description:
+ "Make web search available to models from all providers (default: true). Set to false to limit it to managed providers.",
}), // kilocode_change
attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({
description: "Attachment processing configuration, including image size limits and resizing behavior",
diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx
index 9dcb20b6e05..fae54f6b8cb 100644
--- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx
+++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx
@@ -12,7 +12,7 @@ export function ToolsRoute() {
const [search, setSearch] = createSignal("")
const snap = () => ctx.data()
const websearch = createMemo(() => snap()?.overlay.fields.web_search)
- const searchEnabled = createMemo(() => websearch()?.value === true)
+ const searchEnabled = createMemo(() => websearch()?.value !== false)
const rows = createMemo(() => {
const data = snap()
if (!data) return []
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
index fabc7858b8a..ca870fef21a 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
@@ -86,7 +86,7 @@ const BrowserTab: Component = () => {
description={t("settings.webTools.webSearch.description")}
last
>
-
+
{t("settings.webTools.webSearch.title")}
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 4df567dd6bf..4f7c275cfa5 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -377,7 +377,7 @@ export const layer: Layer.Layer<
const filtered = (yield* all()).filter((tool) => {
if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change
if (tool.id === WebSearchTool.id) {
- if (cfg.web_search === true) return true // kilocode_change
+ if (cfg.web_search !== false) return true // kilocode_change
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
}
diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts
index 51902e15142..39389503067 100644
--- a/packages/opencode/test/kilocode/config/config.test.ts
+++ b/packages/opencode/test/kilocode/config/config.test.ts
@@ -152,10 +152,10 @@ describe("global config updates", () => {
})
describe("kilocode web search config", () => {
- test("accepts the websearch availability setting", () => {
- const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true })
+ test("accepts explicitly limiting web search to managed providers", () => {
+ const config = Schema.decodeUnknownSync(Config.Info)({ web_search: false })
- expect(config.web_search).toBe(true)
+ expect(config.web_search).toBe(false)
})
})
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index 97e4e6c51c1..2420506b9de 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -125,7 +125,21 @@ const websearch = testEffect(
config: {
get: () =>
Effect.succeed({
- web_search: true,
+ provider: { openai: { options: { apiKey: "test-openai-key" } } },
+ }),
+ },
+ }),
+ node,
+ Agent.defaultLayer,
+ ),
+)
+const websearchOff = testEffect(
+ Layer.mergeAll(
+ registryLayer({
+ config: {
+ get: () =>
+ Effect.succeed({
+ web_search: false,
provider: { openai: { options: { apiKey: "test-openai-key" } } },
}),
},
@@ -155,23 +169,7 @@ function sandboxProfile(): Profile {
describe("tool.registry", () => {
// kilocode_change start
- it.instance("hides websearch for a third-party provider by default", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- const agent = yield* Agent.Service
- const build = yield* agent.get("build")
- if (!build) return yield* Effect.die(new Error("build agent not found"))
- const tools = yield* registry.tools({
- providerID: ProviderV2.ID.openai,
- modelID: ModelV2.ID.make("test"),
- agent: build,
- })
-
- expect(tools.map((tool) => tool.id)).not.toContain("websearch")
- }),
- )
-
- websearch.instance("shows websearch for a configured third-party provider when enabled", () =>
+ websearch.instance("shows websearch by default for a configured third-party provider", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const agent = yield* Agent.Service
@@ -187,6 +185,22 @@ describe("tool.registry", () => {
}),
)
+ websearchOff.instance("hides websearch for a configured third-party provider when disabled", () =>
+ Effect.gen(function* () {
+ const registry = yield* ToolRegistry.Service
+ const agent = yield* Agent.Service
+ const build = yield* agent.get("build")
+ if (!build) return yield* Effect.die(new Error("build agent not found"))
+ const tools = yield* registry.tools({
+ providerID: ProviderV2.ID.openai,
+ modelID: ModelV2.ID.make("test"),
+ agent: build,
+ })
+
+ expect(tools.map((tool) => tool.id)).not.toContain("websearch")
+ }),
+ )
+
sandboxed.instance("preserves built-in network classification through production tool definition processing", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
From c5d3032118aa87cdecb3ae40f1117344762efd2b Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Mon, 27 Jul 2026 14:47:11 -0400
Subject: [PATCH 05/71] fix: keep third-party web search opt-in
---
.changeset/enable-websearch-config.md | 2 +-
packages/core/src/v1/config/config.ts | 3 +-
.../src/routes/config/ToolsRoute.tsx | 2 +-
.../src/components/settings/BrowserTab.tsx | 2 +-
packages/opencode/src/tool/registry.ts | 2 +-
.../test/kilocode/config/config.test.ts | 6 +--
packages/opencode/test/tool/registry.test.ts | 50 +++++++------------
7 files changed, 26 insertions(+), 41 deletions(-)
diff --git a/.changeset/enable-websearch-config.md b/.changeset/enable-websearch-config.md
index ebbc4135f3b..cd47a9e9fb5 100644
--- a/.changeset/enable-websearch-config.md
+++ b/.changeset/enable-websearch-config.md
@@ -3,4 +3,4 @@
"kilo-code": patch
---
-Configure web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings.
+Allow users to enable web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings.
diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts
index 0ef9d1ffc71..fea9a7f5989 100644
--- a/packages/core/src/v1/config/config.ts
+++ b/packages/core/src/v1/config/config.ts
@@ -227,8 +227,7 @@ export const Info = Schema.Struct({
permission: Schema.optional(ConfigPermissionV1.Info),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
web_search: Schema.optional(Schema.Boolean).annotate({
- description:
- "Make web search available to models from all providers (default: true). Set to false to limit it to managed providers.",
+ description: "Make web search available to models from all providers (default: false)",
}), // kilocode_change
attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({
description: "Attachment processing configuration, including image size limits and resizing behavior",
diff --git a/packages/kilo-console/src/routes/config/ToolsRoute.tsx b/packages/kilo-console/src/routes/config/ToolsRoute.tsx
index fae54f6b8cb..9dcb20b6e05 100644
--- a/packages/kilo-console/src/routes/config/ToolsRoute.tsx
+++ b/packages/kilo-console/src/routes/config/ToolsRoute.tsx
@@ -12,7 +12,7 @@ export function ToolsRoute() {
const [search, setSearch] = createSignal("")
const snap = () => ctx.data()
const websearch = createMemo(() => snap()?.overlay.fields.web_search)
- const searchEnabled = createMemo(() => websearch()?.value !== false)
+ const searchEnabled = createMemo(() => websearch()?.value === true)
const rows = createMemo(() => {
const data = snap()
if (!data) return []
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
index ca870fef21a..fabc7858b8a 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/BrowserTab.tsx
@@ -86,7 +86,7 @@ const BrowserTab: Component = () => {
description={t("settings.webTools.webSearch.description")}
last
>
-
+
{t("settings.webTools.webSearch.title")}
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 4f7c275cfa5..4df567dd6bf 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -377,7 +377,7 @@ export const layer: Layer.Layer<
const filtered = (yield* all()).filter((tool) => {
if (!KiloToolRegistry.available(tool, input.agent)) return false // kilocode_change
if (tool.id === WebSearchTool.id) {
- if (cfg.web_search !== false) return true // kilocode_change
+ if (cfg.web_search === true) return true // kilocode_change
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
}
diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts
index 39389503067..c384d76dff0 100644
--- a/packages/opencode/test/kilocode/config/config.test.ts
+++ b/packages/opencode/test/kilocode/config/config.test.ts
@@ -152,10 +152,10 @@ describe("global config updates", () => {
})
describe("kilocode web search config", () => {
- test("accepts explicitly limiting web search to managed providers", () => {
- const config = Schema.decodeUnknownSync(Config.Info)({ web_search: false })
+ test("accepts enabling web search for all providers", () => {
+ const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true })
- expect(config.web_search).toBe(false)
+ expect(config.web_search).toBe(true)
})
})
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index 2420506b9de..97e4e6c51c1 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -125,21 +125,7 @@ const websearch = testEffect(
config: {
get: () =>
Effect.succeed({
- provider: { openai: { options: { apiKey: "test-openai-key" } } },
- }),
- },
- }),
- node,
- Agent.defaultLayer,
- ),
-)
-const websearchOff = testEffect(
- Layer.mergeAll(
- registryLayer({
- config: {
- get: () =>
- Effect.succeed({
- web_search: false,
+ web_search: true,
provider: { openai: { options: { apiKey: "test-openai-key" } } },
}),
},
@@ -169,23 +155,7 @@ function sandboxProfile(): Profile {
describe("tool.registry", () => {
// kilocode_change start
- websearch.instance("shows websearch by default for a configured third-party provider", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- const agent = yield* Agent.Service
- const build = yield* agent.get("build")
- if (!build) return yield* Effect.die(new Error("build agent not found"))
- const tools = yield* registry.tools({
- providerID: ProviderV2.ID.openai,
- modelID: ModelV2.ID.make("test"),
- agent: build,
- })
-
- expect(tools.map((tool) => tool.id)).toContain("websearch")
- }),
- )
-
- websearchOff.instance("hides websearch for a configured third-party provider when disabled", () =>
+ it.instance("hides websearch for a third-party provider by default", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const agent = yield* Agent.Service
@@ -201,6 +171,22 @@ describe("tool.registry", () => {
}),
)
+ websearch.instance("shows websearch for a configured third-party provider when enabled", () =>
+ Effect.gen(function* () {
+ const registry = yield* ToolRegistry.Service
+ const agent = yield* Agent.Service
+ const build = yield* agent.get("build")
+ if (!build) return yield* Effect.die(new Error("build agent not found"))
+ const tools = yield* registry.tools({
+ providerID: ProviderV2.ID.openai,
+ modelID: ModelV2.ID.make("test"),
+ agent: build,
+ })
+
+ expect(tools.map((tool) => tool.id)).toContain("websearch")
+ }),
+ )
+
sandboxed.instance("preserves built-in network classification through production tool definition processing", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
From 5325cc711b268a27ede10f2430ca4d66d887a33e Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Mon, 27 Jul 2026 15:39:58 -0400
Subject: [PATCH 06/71] docs: add NVIDIA to BYOK providers
Cloud now supports NVIDIA as a direct BYOK provider. Documents the
`nvidia-byok` key ID, the required model prefix, tool-calling
requirement, and NVIDIA's Developer Program endpoint restrictions.
---
packages/kilo-docs/pages/gateway/authentication.md | 1 +
packages/kilo-docs/pages/getting-started/byok.md | 11 +++++++++++
2 files changed, 12 insertions(+)
diff --git a/packages/kilo-docs/pages/gateway/authentication.md b/packages/kilo-docs/pages/gateway/authentication.md
index 30129525ed0..ba6c3a794f6 100644
--- a/packages/kilo-docs/pages/gateway/authentication.md
+++ b/packages/kilo-docs/pages/gateway/authentication.md
@@ -97,6 +97,7 @@ BYOK lets you use your own provider API keys with the Kilo AI Gateway. When a BY
| Kimi Code | `kimi-coding` |
| Martian | `martian` |
| Neuralwatt | `neuralwatt` |
+| NVIDIA | `nvidia-byok` |
| Ollama Cloud | `ollama-cloud` |
| OpenCode Go | `opencode-go` |
| OrcaRouter | `orcarouter` |
diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md
index adb426172dc..bf3d3ff842b 100644
--- a/packages/kilo-docs/pages/getting-started/byok.md
+++ b/packages/kilo-docs/pages/getting-started/byok.md
@@ -49,6 +49,7 @@ These providers offer coding-focused subscriptions or dedicated endpoints. Bring
- Martian
- Mistral Codestral
- Neuralwatt
+- NVIDIA
- Ollama Cloud
- OpenCode Go
- OrcaRouter
@@ -87,6 +88,16 @@ Your IAM user or role must have the following permissions:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
+### NVIDIA configuration
+
+Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider.
+
+NVIDIA models use their own `nvidia-byok/` prefix, so select a model such as `nvidia-byok/nvidia/nemotron-3-super-120b-a12b` rather than a Kilo Gateway model with the same name. Only NVIDIA-hosted models that support tool calling are available.
+
+{% callout type="warning" title="NVIDIA API Catalog terms" %}
+NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
+{% /callout %}
+
## How Bring Your Own Key works
- When you use the **Kilo Gateway** provider, Kilo checks if there's a BYOK key for the selected model's provider.
From 62d290f9b52db297cf5022b27601c81508ee66c2 Mon Sep 17 00:00:00 2001
From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:58:08 -0400
Subject: [PATCH 07/71] Apply suggestion from @lambertjosh
---
packages/kilo-docs/pages/getting-started/byok.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md
index bf3d3ff842b..43f00eac318 100644
--- a/packages/kilo-docs/pages/getting-started/byok.md
+++ b/packages/kilo-docs/pages/getting-started/byok.md
@@ -92,7 +92,7 @@ Your IAM user or role must have the following permissions:
Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider.
-NVIDIA models use their own `nvidia-byok/` prefix, so select a model such as `nvidia-byok/nvidia/nemotron-3-super-120b-a12b` rather than a Kilo Gateway model with the same name. Only NVIDIA-hosted models that support tool calling are available.
+Models that do not support tool calling are excluded, as they are unlikely to be provide useful results.
{% callout type="warning" title="NVIDIA API Catalog terms" %}
NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
From 605dc483e84fbbc3ff5e213e057f3791a14e9299 Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Mon, 27 Jul 2026 17:30:02 -0400
Subject: [PATCH 08/71] chore(sdk): minimize web search schema diff
---
packages/sdk/openapi.json | 5637 +++++++++++++++++++++++++++----------
1 file changed, 4111 insertions(+), 1526 deletions(-)
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 1451c3a6b3f..7bd369f6bb0 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -4211,10 +4211,10 @@
]
}
},
- "/experimental/project/{projectID}/copy": {
+ "/experimental/project/{projectID}/copy/generate-name": {
"post": {
"tags": ["projectCopy"],
- "operationId": "experimental.projectCopy.create",
+ "operationId": "experimental.projectCopy.generateName",
"parameters": [
{
"name": "projectID",
@@ -4224,6 +4224,14 @@
},
"required": true
},
+ {
+ "name": "directory",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ },
+ "required": false
+ },
{
"name": "workspace",
"in": "query",
@@ -4235,56 +4243,45 @@
],
"responses": {
"200": {
- "description": "Project copy created",
+ "description": "Success",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ProjectCopyCopy"
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["name"],
+ "additionalProperties": false
}
}
}
},
"400": {
- "description": "ProjectCopyError | InvalidRequestError",
+ "description": "Bad request",
"content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ProjectCopyError"
- },
- {
- "$ref": "#/components/schemas/InvalidRequestError"
- }
- ]
+ "$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
- "description": "Create a local physical copy of a project using the selected strategy.",
- "summary": "Create project copy",
+ "description": "Generate a short name for a project copy from task context.",
+ "summary": "Generate project copy name",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
- "strategy": {
- "type": "string",
- "enum": ["git_worktree"]
- },
- "directory": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
"context": {
"type": "string"
}
},
- "required": ["strategy", "directory"],
"additionalProperties": false
}
}
@@ -4293,145 +4290,7 @@
"x-codeSamples": [
{
"lang": "js",
- "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.create({\n ...\n})"
- }
- ]
- },
- "delete": {
- "tags": ["projectCopy"],
- "operationId": "experimental.projectCopy.remove",
- "parameters": [
- {
- "name": "projectID",
- "in": "path",
- "schema": {
- "type": "string"
- },
- "required": true
- },
- {
- "name": "directory",
- "in": "query",
- "schema": {
- "type": "string"
- },
- "required": false
- },
- {
- "name": "workspace",
- "in": "query",
- "schema": {
- "type": "string"
- },
- "required": false
- }
- ],
- "responses": {
- "204": {
- "description": "Project copy removed"
- },
- "400": {
- "description": "ProjectCopyError | InvalidRequestError",
- "content": {
- "application/json": {
- "schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ProjectCopyError"
- },
- {
- "$ref": "#/components/schemas/InvalidRequestError"
- }
- ]
- }
- }
- }
- }
- },
- "description": "Remove a local physical copy of a project using the selected strategy.",
- "summary": "Remove project copy",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "directory": {
- "type": "string"
- }
- },
- "required": ["directory"],
- "additionalProperties": false
- }
- }
- }
- },
- "x-codeSamples": [
- {
- "lang": "js",
- "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.remove({\n ...\n})"
- }
- ]
- }
- },
- "/experimental/project/{projectID}/copy/refresh": {
- "post": {
- "tags": ["projectCopy"],
- "operationId": "experimental.projectCopy.refresh",
- "parameters": [
- {
- "name": "projectID",
- "in": "path",
- "schema": {
- "type": "string"
- },
- "required": true
- },
- {
- "name": "directory",
- "in": "query",
- "schema": {
- "type": "string"
- },
- "required": false
- },
- {
- "name": "workspace",
- "in": "query",
- "schema": {
- "type": "string"
- },
- "required": false
- }
- ],
- "responses": {
- "204": {
- "description": "Project copies refreshed"
- },
- "400": {
- "description": "ProjectCopyError | InvalidRequestError",
- "content": {
- "application/json": {
- "schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ProjectCopyError"
- },
- {
- "$ref": "#/components/schemas/InvalidRequestError"
- }
- ]
- }
- }
- }
- }
- },
- "description": "Discover local project copies using one or all configured strategies.",
- "summary": "Refresh project copies",
- "x-codeSamples": [
- {
- "lang": "js",
- "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.refresh({\n ...\n})"
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.projectCopy.generateName({\n ...\n})"
}
]
}
@@ -14735,6 +14594,16 @@
}
}
}
+ },
+ "500": {
+ "description": "CloudSessionImportError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CloudSessionImportError"
+ }
+ }
+ }
}
},
"description": "Download a cloud-synced session and write it to local storage with fresh IDs.",
@@ -16890,6 +16759,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -19898,7 +19771,7 @@
},
"/api/health": {
"get": {
- "tags": ["kilo experimental HttpApi"],
+ "tags": ["Kilo HttpApi"],
"operationId": "v2.health.get",
"parameters": [],
"security": [],
@@ -19942,8 +19815,8 @@
}
}
},
- "description": "Check whether the v2 API server is ready to accept requests.",
- "summary": "Check v2 server health",
+ "description": "Check whether the API server is ready to accept requests.",
+ "summary": "Check server health",
"x-codeSamples": [
{
"lang": "js",
@@ -19952,9 +19825,77 @@
]
}
},
+ "/api/location": {
+ "get": {
+ "tags": ["Kilo HttpApi"],
+ "operationId": "v2.location.get",
+ "parameters": [
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Location.Info",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LocationInfo"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Resolve the requested location or the server default location.",
+ "summary": "Get location",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.location.get({\n ...\n})"
+ }
+ ]
+ }
+ },
"/api/agent": {
"get": {
- "tags": ["kilo experimental HttpApi"],
+ "tags": ["Kilo HttpApi"],
"operationId": "v2.agent.list",
"parameters": [
{
@@ -20023,8 +19964,8 @@
}
}
},
- "description": "Retrieve currently registered v2 agents.",
- "summary": "List v2 agents",
+ "description": "Retrieve currently registered agents.",
+ "summary": "List agents",
"x-codeSamples": [
{
"lang": "js",
@@ -20035,7 +19976,7 @@
},
"/api/session": {
"get": {
- "tags": ["v2"],
+ "tags": ["sessions"],
"operationId": "v2.session.list",
"parameters": [
{
@@ -20109,11 +20050,11 @@
"security": [],
"responses": {
"200": {
- "description": "V2SessionsResponse",
+ "description": "SessionsResponse",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/V2SessionsResponse"
+ "$ref": "#/components/schemas/SessionsResponse"
}
}
}
@@ -20150,18 +20091,192 @@
}
},
"description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
- "summary": "List v2 sessions",
+ "summary": "List sessions",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.list({\n ...\n})"
}
]
+ },
+ "post": {
+ "tags": ["sessions"],
+ "operationId": "v2.session.create",
+ "parameters": [],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionV2Info"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Create a session at the requested location.",
+ "summary": "Create session",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "providerID": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "providerID"],
+ "additionalProperties": false
+ },
+ "location": {
+ "$ref": "#/components/schemas/LocationRef"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.create({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/session/{sessionID}": {
+ "get": {
+ "tags": ["sessions"],
+ "operationId": "v2.session.get",
+ "parameters": [
+ {
+ "name": "sessionID",
+ "in": "path",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionV2Info"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "SessionNotFoundError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "description": "Retrieve a session by ID.",
+ "summary": "Get session",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.get({\n ...\n})"
+ }
+ ]
}
},
"/api/session/{sessionID}/prompt": {
"post": {
- "tags": ["v2"],
+ "tags": ["sessions"],
"operationId": "v2.session.prompt",
"parameters": [
{
@@ -20218,7 +20333,14 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
}
}
}
@@ -20234,8 +20356,8 @@
}
}
},
- "description": "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.",
- "summary": "Send v2 message",
+ "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.",
+ "summary": "Send message",
"requestBody": {
"content": {
"application/json": {
@@ -20274,7 +20396,7 @@
},
"/api/session/{sessionID}/compact": {
"post": {
- "tags": ["v2"],
+ "tags": ["sessions"],
"operationId": "v2.session.compact",
"parameters": [
{
@@ -20317,7 +20439,14 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
}
}
}
@@ -20333,8 +20462,8 @@
}
}
},
- "description": "Compact a v2 session conversation.",
- "summary": "Compact v2 session",
+ "description": "Compact a session conversation.",
+ "summary": "Compact session",
"x-codeSamples": [
{
"lang": "js",
@@ -20345,7 +20474,7 @@
},
"/api/session/{sessionID}/wait": {
"post": {
- "tags": ["v2"],
+ "tags": ["sessions"],
"operationId": "v2.session.wait",
"parameters": [
{
@@ -20388,7 +20517,14 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
}
}
}
@@ -20404,8 +20540,8 @@
}
}
},
- "description": "Wait for a v2 session agent loop to become idle.",
- "summary": "Wait for v2 session",
+ "description": "Wait for a session agent loop to become idle.",
+ "summary": "Wait for session",
"x-codeSamples": [
{
"lang": "js",
@@ -20416,7 +20552,7 @@
},
"/api/session/{sessionID}/context": {
"get": {
- "tags": ["v2"],
+ "tags": ["sessions"],
"operationId": "v2.session.context",
"parameters": [
{
@@ -20476,7 +20612,14 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
}
}
}
@@ -20492,8 +20635,8 @@
}
}
},
- "description": "Retrieve the active context messages for a v2 session (all messages after the last compaction).",
- "summary": "Get v2 session context",
+ "description": "Retrieve the active context messages for a session (all messages after the last compaction).",
+ "summary": "Get session context",
"x-codeSamples": [
{
"lang": "js",
@@ -20504,7 +20647,7 @@
},
"/api/session/{sessionID}/message": {
"get": {
- "tags": ["v2 messages"],
+ "tags": ["messages"],
"operationId": "v2.session.messages",
"parameters": [
{
@@ -20546,11 +20689,11 @@
"security": [],
"responses": {
"200": {
- "description": "V2SessionMessagesResponse",
+ "description": "SessionMessagesResponse",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/V2SessionMessagesResponse"
+ "$ref": "#/components/schemas/SessionMessagesResponse"
}
}
}
@@ -20587,7 +20730,14 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
}
}
}
@@ -20603,8 +20753,8 @@
}
}
},
- "description": "Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
- "summary": "Get v2 session messages",
+ "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
+ "summary": "Get session messages",
"x-codeSamples": [
{
"lang": "js",
@@ -20615,7 +20765,7 @@
},
"/api/model": {
"get": {
- "tags": ["v2 models"],
+ "tags": ["models"],
"operationId": "v2.model.list",
"parameters": [
{
@@ -20694,8 +20844,8 @@
}
}
},
- "description": "Retrieve available v2 models ordered by release date.",
- "summary": "List v2 models",
+ "description": "Retrieve available models ordered by release date.",
+ "summary": "List models",
"x-codeSamples": [
{
"lang": "js",
@@ -20706,7 +20856,7 @@
},
"/api/provider": {
"get": {
- "tags": ["v2 providers"],
+ "tags": ["providers"],
"operationId": "v2.provider.list",
"parameters": [
{
@@ -20785,8 +20935,8 @@
}
}
},
- "description": "Retrieve active v2 AI providers so clients can show provider availability and configuration.",
- "summary": "List v2 providers",
+ "description": "Retrieve active AI providers so clients can show provider availability and configuration.",
+ "summary": "List providers",
"x-codeSamples": [
{
"lang": "js",
@@ -20797,7 +20947,7 @@
},
"/api/provider/{providerID}": {
"get": {
- "tags": ["v2 providers"],
+ "tags": ["providers"],
"operationId": "v2.provider.get",
"parameters": [
{
@@ -20891,8 +21041,8 @@
}
}
},
- "description": "Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.",
- "summary": "Get v2 provider",
+ "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
+ "summary": "Get provider",
"x-codeSamples": [
{
"lang": "js",
@@ -20901,9 +21051,1011 @@
]
}
},
+ "/api/integration": {
+ "get": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.list",
+ "parameters": [
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/IntegrationInfo"
+ }
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Retrieve available integrations and their authentication methods.",
+ "summary": "List integrations",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/integration/{integrationID}": {
+ "get": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.get",
+ "parameters": [
+ {
+ "name": "integrationID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "$ref": "#/components/schemas/IntegrationInfo"
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Retrieve one integration and its authentication methods.",
+ "summary": "Get integration",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.get({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/integration/{integrationID}/connect/key": {
+ "post": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.connect.key",
+ "parameters": [
+ {
+ "name": "integrationID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Run a key authentication method and store the resulting credential.",
+ "summary": "Connect with key",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["key"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.connect.key({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/integration/{integrationID}/connect/oauth": {
+ "post": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.connect.oauth",
+ "parameters": [
+ {
+ "name": "integrationID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "$ref": "#/components/schemas/IntegrationAttempt"
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Start an OAuth attempt and return the authorization details.",
+ "summary": "Begin OAuth connection",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "methodID": {
+ "type": "string"
+ },
+ "inputs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["methodID", "inputs"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.connect.oauth({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/integration/attempt/{attemptID}": {
+ "get": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.attempt.status",
+ "parameters": [
+ {
+ "name": "attemptID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["pending"]
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "expires": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ }
+ },
+ "required": ["created", "expires"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["status", "time"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["complete"]
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "expires": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ }
+ },
+ "required": ["created", "expires"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["status", "time"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["failed"]
+ },
+ "message": {
+ "type": "string"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "expires": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ }
+ },
+ "required": ["created", "expires"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["status", "message", "time"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["expired"]
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "expires": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ }
+ },
+ "required": ["created", "expires"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["status", "time"],
+ "additionalProperties": false
+ }
+ ]
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Poll the current status of an OAuth attempt.",
+ "summary": "Get OAuth attempt status",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.status({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.attempt.cancel",
+ "parameters": [
+ {
+ "name": "attemptID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Cancel an OAuth attempt and release its resources.",
+ "summary": "Cancel OAuth connection",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.cancel({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/integration/attempt/{attemptID}/complete": {
+ "post": {
+ "tags": ["integrations"],
+ "operationId": "v2.integration.attempt.complete",
+ "parameters": [
+ {
+ "name": "attemptID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Complete a code-based OAuth attempt and store the resulting credential.",
+ "summary": "Complete OAuth connection",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.integration.attempt.complete({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/credential/{credentialID}": {
+ "patch": {
+ "tags": ["Kilo HttpApi"],
+ "operationId": "v2.credential.update",
+ "parameters": [
+ {
+ "name": "credentialID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Update a stored credential label.",
+ "summary": "Update credential",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["label"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.credential.update({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "tags": ["Kilo HttpApi"],
+ "operationId": "v2.credential.remove",
+ "parameters": [
+ {
+ "name": "credentialID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Remove a stored integration credential.",
+ "summary": "Remove credential",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.credential.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
"/api/permission/request": {
"get": {
- "tags": ["v2 permissions"],
+ "tags": ["permissions"],
"operationId": "v2.permission.request.list",
"parameters": [
{
@@ -20982,184 +22134,9 @@
]
}
},
- "/api/session/{sessionID}/permission/request": {
- "get": {
- "tags": ["v2 session permissions"],
- "operationId": "v2.session.permission.list",
- "parameters": [
- {
- "name": "sessionID",
- "in": "path",
- "schema": {
- "type": "string",
- "pattern": "^ses.*"
- },
- "required": true
- }
- ],
- "security": [],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/PermissionV2Request"
- }
- }
- },
- "required": ["data"],
- "additionalProperties": false
- }
- }
- }
- },
- "400": {
- "description": "InvalidRequestError",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/InvalidRequestError"
- }
- }
- }
- },
- "401": {
- "description": "UnauthorizedError",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UnauthorizedError"
- }
- }
- }
- },
- "404": {
- "description": "SessionNotFoundError",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/SessionNotFoundError"
- }
- }
- }
- }
- },
- "description": "Retrieve pending permission requests owned by a session.",
- "summary": "List session permission requests",
- "x-codeSamples": [
- {
- "lang": "js",
- "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.list({\n ...\n})"
- }
- ]
- }
- },
- "/api/session/{sessionID}/permission/request/{requestID}/reply": {
- "post": {
- "tags": ["v2 session permissions"],
- "operationId": "v2.session.permission.reply",
- "parameters": [
- {
- "name": "sessionID",
- "in": "path",
- "schema": {
- "type": "string",
- "pattern": "^ses.*"
- },
- "required": true
- },
- {
- "name": "requestID",
- "in": "path",
- "schema": {
- "type": "string",
- "pattern": "^per"
- },
- "required": true
- }
- ],
- "security": [],
- "responses": {
- "204": {
- "description": ""
- },
- "400": {
- "description": "InvalidRequestError",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/InvalidRequestError"
- }
- }
- }
- },
- "401": {
- "description": "UnauthorizedError",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UnauthorizedError"
- }
- }
- }
- },
- "404": {
- "description": "SessionNotFoundError | PermissionNotFoundError",
- "content": {
- "application/json": {
- "schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/SessionNotFoundError"
- },
- {
- "$ref": "#/components/schemas/PermissionNotFoundError"
- }
- ]
- }
- }
- }
- }
- },
- "description": "Respond to a pending permission request owned by a session.",
- "summary": "Reply to pending permission request",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "reply": {
- "$ref": "#/components/schemas/PermissionV2Reply"
- },
- "message": {
- "type": "string"
- }
- },
- "required": ["reply"],
- "additionalProperties": false
- }
- }
- },
- "required": true
- },
- "x-codeSamples": [
- {
- "lang": "js",
- "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.reply({\n ...\n})"
- }
- ]
- }
- },
"/api/permission/saved": {
"get": {
- "tags": ["v2 saved permissions"],
+ "tags": ["permissions"],
"operationId": "v2.permission.saved.list",
"parameters": [
{
@@ -21226,7 +22203,7 @@
},
"/api/permission/saved/{id}": {
"delete": {
- "tags": ["v2 saved permissions"],
+ "tags": ["permissions"],
"operationId": "v2.permission.saved.remove",
"parameters": [
{
@@ -21274,9 +22251,194 @@
]
}
},
- "/api/fs/read": {
+ "/api/session/{sessionID}/permission": {
"get": {
- "tags": ["v2 filesystem"],
+ "tags": ["permissions"],
+ "operationId": "v2.session.permission.list",
+ "parameters": [
+ {
+ "name": "sessionID",
+ "in": "path",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PermissionV2Request"
+ }
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "SessionNotFoundError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "description": "Retrieve pending permission requests owned by a session.",
+ "summary": "List session permission requests",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/session/{sessionID}/permission/{requestID}/reply": {
+ "post": {
+ "tags": ["permissions"],
+ "operationId": "v2.session.permission.reply",
+ "parameters": [
+ {
+ "name": "sessionID",
+ "in": "path",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ },
+ {
+ "name": "requestID",
+ "in": "path",
+ "schema": {
+ "type": "string",
+ "pattern": "^per"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "SessionNotFoundError | PermissionNotFoundError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/PermissionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "description": "Respond to a pending permission request owned by a session.",
+ "summary": "Reply to pending permission request",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "reply": {
+ "$ref": "#/components/schemas/PermissionV2Reply"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["reply"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": true
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.permission.reply({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/fs/read/*": {
+ "get": {
+ "tags": ["filesystem"],
"operationId": "v2.fs.read",
"parameters": [
{
@@ -21304,14 +22466,6 @@
"schema": {
"type": "string"
},
- "required": true
- },
- {
- "name": "reference",
- "in": "query",
- "schema": {
- "type": "string"
- },
"required": false
}
],
@@ -21320,26 +22474,10 @@
"200": {
"description": "Success",
"content": {
- "application/json": {
+ "application/octet-stream": {
"schema": {
- "type": "object",
- "properties": {
- "location": {
- "$ref": "#/components/schemas/LocationInfo"
- },
- "data": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/FileSystemTextContent"
- },
- {
- "$ref": "#/components/schemas/FileSystemBinaryContent"
- }
- ]
- }
- },
- "required": ["location", "data"],
- "additionalProperties": false
+ "type": "string",
+ "format": "binary"
}
}
}
@@ -21365,7 +22503,7 @@
}
}
},
- "description": "Read one file relative to the requested location.",
+ "description": "Serve one file relative to the requested location.",
"summary": "Read file",
"x-codeSamples": [
{
@@ -21377,7 +22515,7 @@
},
"/api/fs/list": {
"get": {
- "tags": ["v2 filesystem"],
+ "tags": ["filesystem"],
"operationId": "v2.fs.list",
"parameters": [
{
@@ -21406,14 +22544,6 @@
"type": "string"
},
"required": false
- },
- {
- "name": "reference",
- "in": "query",
- "schema": {
- "type": "string"
- },
- "required": false
}
],
"security": [],
@@ -21472,9 +22602,115 @@
]
}
},
+ "/api/fs/find": {
+ "get": {
+ "tags": ["filesystem"],
+ "operationId": "v2.fs.find",
+ "parameters": [
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ },
+ {
+ "name": "query",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "type",
+ "in": "query",
+ "schema": {
+ "type": "string",
+ "enum": ["file", "directory"]
+ },
+ "required": false
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ },
+ "required": false
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileSystemEntry"
+ }
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "Find recursively ranked filesystem entries relative to the requested location.",
+ "summary": "Find files",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.fs.find({\n ...\n})"
+ }
+ ]
+ }
+ },
"/api/command": {
"get": {
- "tags": ["v2 commands"],
+ "tags": ["commands"],
"operationId": "v2.command.list",
"parameters": [
{
@@ -21543,8 +22779,8 @@
}
}
},
- "description": "Retrieve currently registered v2 commands.",
- "summary": "List v2 commands",
+ "description": "Retrieve currently registered commands.",
+ "summary": "List commands",
"x-codeSamples": [
{
"lang": "js",
@@ -21555,7 +22791,7 @@
},
"/api/skill": {
"get": {
- "tags": ["v2 skills"],
+ "tags": ["skills"],
"operationId": "v2.skill.list",
"parameters": [
{
@@ -21624,8 +22860,8 @@
}
}
},
- "description": "Retrieve currently registered v2 skills.",
- "summary": "List v2 skills",
+ "description": "Retrieve currently registered skills.",
+ "summary": "List skills",
"x-codeSamples": [
{
"lang": "js",
@@ -21636,7 +22872,7 @@
},
"/api/event": {
"get": {
- "tags": ["v2 events"],
+ "tags": ["events"],
"operationId": "v2.event.subscribe",
"parameters": [
{
@@ -21692,8 +22928,8 @@
}
}
},
- "description": "Subscribe to native EventV2 payloads for a location.",
- "summary": "Subscribe to v2 events",
+ "description": "Subscribe to native event payloads for a location.",
+ "summary": "Subscribe to events",
"x-codeSamples": [
{
"lang": "js",
@@ -21704,7 +22940,7 @@
},
"/api/question/request": {
"get": {
- "tags": ["v2 questions"],
+ "tags": ["session questions"],
"operationId": "v2.question.request.list",
"parameters": [
{
@@ -21783,9 +23019,94 @@
]
}
},
- "/api/session/{sessionID}/question/request/{requestID}/reply": {
+ "/api/session/{sessionID}/question": {
+ "get": {
+ "tags": ["session questions"],
+ "operationId": "v2.session.question.list",
+ "parameters": [
+ {
+ "name": "sessionID",
+ "in": "path",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionV2Request"
+ }
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "SessionNotFoundError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ },
+ {
+ "$ref": "#/components/schemas/SessionNotFoundError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "description": "Retrieve pending question requests owned by a session.",
+ "summary": "List session question requests",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.session.question.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/api/session/{sessionID}/question/{requestID}/reply": {
"post": {
- "tags": ["v2 session questions"],
+ "tags": ["session questions"],
"operationId": "v2.session.question.reply",
"parameters": [
{
@@ -21838,11 +23159,14 @@
"application/json": {
"schema": {
"anyOf": [
+ {
+ "$ref": "#/components/schemas/QuestionNotFoundError"
+ },
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
- "$ref": "#/components/schemas/QuestionNotFoundError"
+ "$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
@@ -21870,9 +23194,9 @@
]
}
},
- "/api/session/{sessionID}/question/request/{requestID}/reject": {
+ "/api/session/{sessionID}/question/{requestID}/reject": {
"post": {
- "tags": ["v2 session questions"],
+ "tags": ["session questions"],
"operationId": "v2.session.question.reject",
"parameters": [
{
@@ -21925,11 +23249,14 @@
"application/json": {
"schema": {
"anyOf": [
+ {
+ "$ref": "#/components/schemas/QuestionNotFoundError"
+ },
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
- "$ref": "#/components/schemas/QuestionNotFoundError"
+ "$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
@@ -21947,6 +23274,322 @@
]
}
},
+ "/api/reference": {
+ "get": {
+ "tags": ["reference"],
+ "operationId": "v2.reference.list",
+ "parameters": [
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "$ref": "#/components/schemas/LocationInfo"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ReferenceInfo"
+ }
+ }
+ },
+ "required": ["location", "data"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "UnauthorizedError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnauthorizedError"
+ }
+ }
+ }
+ }
+ },
+ "description": "List references available in the requested location.",
+ "summary": "List references",
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.reference.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/project/{projectID}/copy": {
+ "post": {
+ "tags": ["projectCopy"],
+ "operationId": "v2.projectCopy.create",
+ "parameters": [
+ {
+ "name": "projectID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "ProjectCopy.Copy",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectCopyCopy"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "ProjectCopyError | InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ProjectCopyError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "strategy": {
+ "type": "string"
+ },
+ "directory": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["strategy", "directory"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.create({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "tags": ["projectCopy"],
+ "operationId": "v2.projectCopy.remove",
+ "parameters": [
+ {
+ "name": "projectID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "ProjectCopyError | InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ProjectCopyError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "force": {
+ "type": "boolean"
+ }
+ },
+ "required": ["directory", "force"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/project/{projectID}/copy/refresh": {
+ "post": {
+ "tags": ["projectCopy"],
+ "operationId": "v2.projectCopy.refresh",
+ "parameters": [
+ {
+ "name": "projectID",
+ "in": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "name": "location",
+ "in": "query",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "workspace": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "required": false,
+ "style": "deepObject",
+ "explode": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "400": {
+ "description": "ProjectCopyError | InvalidRequestError",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ProjectCopyError"
+ },
+ {
+ "$ref": "#/components/schemas/InvalidRequestError"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.v2.projectCopy.refresh({\n ...\n})"
+ }
+ ]
+ }
+ },
"/pty/{ptyID}/connect": {
"get": {
"tags": ["pty"],
@@ -22176,6 +23819,9 @@
{
"$ref": "#/components/schemas/EventSessionNextPromptPromoted"
},
+ {
+ "$ref": "#/components/schemas/EventSessionNextInterruptRequested"
+ },
{
"$ref": "#/components/schemas/EventSessionNextContextUpdated"
},
@@ -22300,61 +23946,10 @@
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
- "$ref": "#/components/schemas/EventTodoUpdated"
+ "$ref": "#/components/schemas/EventReferenceUpdated"
},
{
- "$ref": "#/components/schemas/EventSessionStatus"
- },
- {
- "$ref": "#/components/schemas/EventSessionIdle"
- },
- {
- "$ref": "#/components/schemas/EventSessionCompacted"
- },
- {
- "$ref": "#/components/schemas/EventCommandExecuted"
- },
- {
- "$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
- },
- {
- "$ref": "#/components/schemas/EventProjectUpdated"
- },
- {
- "$ref": "#/components/schemas/EventLspUpdated"
- },
- {
- "$ref": "#/components/schemas/EventFileEdited"
- },
- {
- "$ref": "#/components/schemas/EventFileWatcherUpdated"
- },
- {
- "$ref": "#/components/schemas/EventVcsBranchUpdated"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceReady"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceFailed"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceStatus"
- },
- {
- "$ref": "#/components/schemas/EventWorktreeReady"
- },
- {
- "$ref": "#/components/schemas/EventWorktreeFailed"
- },
- {
- "$ref": "#/components/schemas/EventAccountAdded"
- },
- {
- "$ref": "#/components/schemas/EventAccountRemoved"
- },
- {
- "$ref": "#/components/schemas/EventAccountSwitched"
+ "$ref": "#/components/schemas/EventIntegrationUpdated"
},
{
"$ref": "#/components/schemas/EventPermissionV2Asked"
@@ -22362,6 +23957,15 @@
{
"$ref": "#/components/schemas/EventPermissionV2Replied"
},
+ {
+ "$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventFileEdited"
+ },
+ {
+ "$ref": "#/components/schemas/EventFileWatcherUpdated"
+ },
{
"$ref": "#/components/schemas/EventPtyCreated"
},
@@ -22383,6 +23987,45 @@
{
"$ref": "#/components/schemas/EventQuestionV2Rejected"
},
+ {
+ "$ref": "#/components/schemas/EventTodoUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionStatus"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionIdle"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionCompacted"
+ },
+ {
+ "$ref": "#/components/schemas/EventCommandExecuted"
+ },
+ {
+ "$ref": "#/components/schemas/EventProjectUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventLspUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventVcsBranchUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceReady"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceFailed"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceStatus"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorktreeReady"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorktreeFailed"
+ },
{
"$ref": "#/components/schemas/EventServerInstanceDisposed"
}
@@ -23353,6 +24996,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -23641,6 +25288,27 @@
"required": ["name", "data"],
"additionalProperties": false
},
+ "ContentFilterError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "enum": ["ContentFilterError"]
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["name", "data"],
+ "additionalProperties": false
+ },
"APIError": {
"type": "object",
"properties": {
@@ -23734,6 +25402,9 @@
{
"$ref": "#/components/schemas/ContextOverflowError"
},
+ {
+ "$ref": "#/components/schemas/ContentFilterError"
+ },
{
"$ref": "#/components/schemas/APIError"
}
@@ -24350,6 +26021,17 @@
},
"snapshot": {
"type": "string"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number",
+ "minimum": 0
+ }
+ },
+ "required": ["start"],
+ "additionalProperties": false
}
},
"required": ["id", "sessionID", "messageID", "type"],
@@ -24393,6 +26075,47 @@
"required": ["providerID", "modelID"],
"additionalProperties": false
},
+ "generationID": {
+ "type": "string"
+ },
+ "vercelID": {
+ "type": "string"
+ },
+ "metrics": {
+ "type": "object",
+ "properties": {
+ "prompt": {
+ "type": "number"
+ },
+ "generation": {
+ "type": "number"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["provider", "computed"]
+ }
+ },
+ "required": ["source"],
+ "additionalProperties": false
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number",
+ "minimum": 0
+ },
+ "end": {
+ "type": "number",
+ "minimum": 0
+ },
+ "elapsed": {
+ "type": "number"
+ }
+ },
+ "required": ["start", "end", "elapsed"],
+ "additionalProperties": false
+ },
"cost": {
"type": "number"
},
@@ -24665,12 +26388,6 @@
"items": {
"$ref": "#/components/schemas/PromptAgentAttachment"
}
- },
- "references": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/PromptReferenceAttachment"
- }
}
},
"required": ["text"],
@@ -24981,6 +26698,70 @@
"required": ["name", "data"],
"additionalProperties": false
},
+ "Pty": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pty"
+ },
+ "title": {
+ "type": "string"
+ },
+ "command": {
+ "type": "string"
+ },
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "cwd": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["running", "exited"]
+ },
+ "pid": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sessionID": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": ["id", "title", "command", "args", "cwd", "status", "pid"],
+ "additionalProperties": false
+ },
+ "Todo": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "Brief description of the task"
+ },
+ "status": {
+ "type": "string",
+ "description": "Current status of the task: pending, in_progress, completed, cancelled"
+ },
+ "priority": {
+ "type": "string",
+ "description": "Priority level of the task: high, medium, low"
+ }
+ },
+ "required": ["content", "status", "priority"],
+ "additionalProperties": false
+ },
"SessionStatus": {
"anyOf": [
{
@@ -25072,51 +26853,6 @@
}
]
},
- "Pty": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^pty"
- },
- "title": {
- "type": "string"
- },
- "command": {
- "type": "string"
- },
- "args": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "cwd": {
- "type": "string"
- },
- "status": {
- "type": "string",
- "enum": ["running", "exited"]
- },
- "pid": {
- "type": "integer",
- "minimum": 0
- },
- "sessionID": {
- "anyOf": [
- {
- "type": "string",
- "pattern": "^ses"
- },
- {
- "type": "null"
- }
- ]
- }
- },
- "required": ["id", "title", "command", "args", "cwd", "status", "pid"],
- "additionalProperties": false
- },
"GlobalEvent": {
"type": "object",
"properties": {
@@ -25269,6 +27005,9 @@
{
"$ref": "#/components/schemas/EventSessionNextPromptPromoted"
},
+ {
+ "$ref": "#/components/schemas/EventSessionNextInterruptRequested"
+ },
{
"$ref": "#/components/schemas/EventSessionNextContextUpdated"
},
@@ -25393,61 +27132,10 @@
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
- "$ref": "#/components/schemas/EventTodoUpdated"
+ "$ref": "#/components/schemas/EventReferenceUpdated"
},
{
- "$ref": "#/components/schemas/EventSessionStatus"
- },
- {
- "$ref": "#/components/schemas/EventSessionIdle"
- },
- {
- "$ref": "#/components/schemas/EventSessionCompacted"
- },
- {
- "$ref": "#/components/schemas/EventCommandExecuted"
- },
- {
- "$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
- },
- {
- "$ref": "#/components/schemas/EventProjectUpdated"
- },
- {
- "$ref": "#/components/schemas/EventLspUpdated"
- },
- {
- "$ref": "#/components/schemas/EventFileEdited"
- },
- {
- "$ref": "#/components/schemas/EventFileWatcherUpdated"
- },
- {
- "$ref": "#/components/schemas/EventVcsBranchUpdated"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceReady"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceFailed"
- },
- {
- "$ref": "#/components/schemas/EventWorkspaceStatus"
- },
- {
- "$ref": "#/components/schemas/EventWorktreeReady"
- },
- {
- "$ref": "#/components/schemas/EventWorktreeFailed"
- },
- {
- "$ref": "#/components/schemas/EventAccountAdded"
- },
- {
- "$ref": "#/components/schemas/EventAccountRemoved"
- },
- {
- "$ref": "#/components/schemas/EventAccountSwitched"
+ "$ref": "#/components/schemas/EventIntegrationUpdated"
},
{
"$ref": "#/components/schemas/EventPermissionV2Asked"
@@ -25455,6 +27143,15 @@
{
"$ref": "#/components/schemas/EventPermissionV2Replied"
},
+ {
+ "$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventFileEdited"
+ },
+ {
+ "$ref": "#/components/schemas/EventFileWatcherUpdated"
+ },
{
"$ref": "#/components/schemas/EventPtyCreated"
},
@@ -25476,6 +27173,45 @@
{
"$ref": "#/components/schemas/EventQuestionV2Rejected"
},
+ {
+ "$ref": "#/components/schemas/EventTodoUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionStatus"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionIdle"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionCompacted"
+ },
+ {
+ "$ref": "#/components/schemas/EventCommandExecuted"
+ },
+ {
+ "$ref": "#/components/schemas/EventProjectUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventLspUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventVcsBranchUpdated"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceReady"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceFailed"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorkspaceStatus"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorktreeReady"
+ },
+ {
+ "$ref": "#/components/schemas/EventWorktreeFailed"
+ },
{
"$ref": "#/components/schemas/EventServerInstanceDisposed"
},
@@ -25575,9 +27311,6 @@
{
"$ref": "#/components/schemas/SyncEventSessionNextCompactionStarted"
},
- {
- "$ref": "#/components/schemas/SyncEventSessionNextCompactionDelta"
- },
{
"$ref": "#/components/schemas/SyncEventSessionNextCompactionEnded"
}
@@ -25618,44 +27351,6 @@
"additionalProperties": false,
"description": "Server configuration for the kilo serve command"
},
- "ReferenceConfigEntry": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "repository": {
- "type": "string",
- "description": "Git repository URL, host/path reference, or GitHub owner/repo shorthand"
- },
- "branch": {
- "type": "string"
- }
- },
- "required": ["repository"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "path": {
- "type": "string",
- "description": "Absolute path, ~/ path, or workspace-relative path to a local reference directory"
- }
- },
- "required": ["path"],
- "additionalProperties": false
- }
- ]
- },
- "ReferenceConfig": {
- "type": "object",
- "additionalProperties": {
- "$ref": "#/components/schemas/ReferenceConfigEntry"
- }
- },
"IndexingConfig": {
"type": "object",
"properties": {
@@ -25850,7 +27545,8 @@
"items": {
"type": "string",
"pattern": "^\\s*\\.?[A-Za-z0-9][A-Za-z0-9_+-]*\\s*$"
- }
+ },
+ "minItems": 1
}
},
"additionalProperties": false
@@ -26213,7 +27909,7 @@
"properties": {
"field": {
"type": "string",
- "enum": ["reasoning_content", "reasoning_details"]
+ "enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
@@ -26539,8 +28235,37 @@
},
"additionalProperties": false
},
+ "references": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "$ref": "#/components/schemas/ConfigV2ReferenceGit"
+ },
+ {
+ "$ref": "#/components/schemas/ConfigV2ReferenceLocal"
+ }
+ ]
+ }
+ },
"reference": {
- "$ref": "#/components/schemas/ReferenceConfig"
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "$ref": "#/components/schemas/ConfigV2ReferenceGit"
+ },
+ {
+ "$ref": "#/components/schemas/ConfigV2ReferenceLocal"
+ }
+ ]
+ }
},
"watcher": {
"type": "object",
@@ -26647,6 +28372,10 @@
"hide_prompt_training_models": {
"type": "boolean"
},
+ "web_search": {
+ "type": "boolean",
+ "description": "Make web search available to models from all providers (default: false)"
+ },
"sandbox": {
"type": "object",
"properties": {
@@ -26905,9 +28634,6 @@
"type": "boolean"
}
},
- "web_search": {
- "type": "boolean"
- },
"attachment": {
"$ref": "#/components/schemas/AttachmentConfig"
},
@@ -27164,7 +28890,7 @@
"properties": {
"field": {
"type": "string",
- "enum": ["reasoning_content", "reasoning_details"]
+ "enum": ["reasoning", "reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
@@ -27809,6 +29535,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -28494,27 +30224,6 @@
"required": ["_tag", "projectID", "message"],
"additionalProperties": false
},
- "ProjectCopyError": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "enum": ["ProjectCopyError"]
- },
- "data": {
- "type": "object",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": ["message"],
- "additionalProperties": false
- }
- },
- "required": ["name", "data"],
- "additionalProperties": false
- },
"PtyNotFoundError": {
"type": "object",
"properties": {
@@ -28977,6 +30686,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29146,6 +30859,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29174,25 +30891,6 @@
}
}
},
- "Todo": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string",
- "description": "Brief description of the task"
- },
- "status": {
- "type": "string",
- "description": "Current status of the task: pending, in_progress, completed, cancelled"
- },
- "priority": {
- "type": "string",
- "description": "Priority level of the task: high, medium, low"
- }
- },
- "required": ["content", "status", "priority"],
- "additionalProperties": false
- },
"Session3": {
"type": "object",
"properties": {
@@ -29353,6 +31051,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29522,6 +31224,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29691,6 +31397,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29860,6 +31570,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30029,6 +31743,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30360,6 +32078,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30529,6 +32251,10 @@
},
"diff": {
"type": "string"
+ },
+ "workspace": {
+ "type": "string",
+ "enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31425,6 +33151,16 @@
"required": ["_tag"],
"additionalProperties": false
},
+ "CloudSessionImportError": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["error"],
+ "additionalProperties": false
+ },
"AgentRequirementResult": {
"type": "object",
"properties": {
@@ -32361,7 +34097,7 @@
"required": ["_tag", "message"],
"additionalProperties": false
},
- "V2SessionsResponse": {
+ "SessionsResponse": {
"type": "object",
"properties": {
"data": {
@@ -32451,7 +34187,7 @@
"required": ["_tag", "message"],
"additionalProperties": false
},
- "V2SessionMessagesResponse": {
+ "SessionMessagesResponse": {
"type": "object",
"properties": {
"data": {
@@ -32493,6 +34229,30 @@
"required": ["_tag", "providerID", "message"],
"additionalProperties": false
},
+ "ProjectCopyError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "enum": ["ProjectCopyError"]
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "forceRequired": {
+ "type": "boolean"
+ }
+ },
+ "required": ["message"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["name", "data"],
+ "additionalProperties": false
+ },
"effect_HttpApiError_Forbidden": {
"type": "object",
"properties": {
@@ -34539,6 +36299,182 @@
"body": {
"type": "object"
},
+ "generation": {
+ "type": "object",
+ "properties": {
+ "maxTokens": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "temperature": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "topP": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "topK": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "frequencyPenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "presencePenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "seed": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "stop": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "options": {
+ "type": "object"
+ },
"variant": {
"type": "string"
}
@@ -34562,6 +36498,182 @@
},
"body": {
"type": "object"
+ },
+ "generation": {
+ "type": "object",
+ "properties": {
+ "maxTokens": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "temperature": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "topP": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "topK": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "frequencyPenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "presencePenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "seed": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "stop": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "options": {
+ "type": "object"
}
},
"required": ["id", "headers", "body"],
@@ -35092,41 +37204,6 @@
"required": ["name"],
"additionalProperties": false
},
- "PromptReferenceAttachment": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "kind": {
- "type": "string",
- "enum": ["local", "git", "invalid"]
- },
- "uri": {
- "type": "string"
- },
- "repository": {
- "type": "string"
- },
- "branch": {
- "type": "string"
- },
- "target": {
- "type": "string"
- },
- "targetUri": {
- "type": "string"
- },
- "problem": {
- "type": "string"
- },
- "source": {
- "$ref": "#/components/schemas/PromptSource"
- }
- },
- "required": ["name", "kind"],
- "additionalProperties": false
- },
"EventSessionNextPrompted": {
"type": "object",
"properties": {
@@ -35243,6 +37320,34 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventSessionNextInterruptRequested": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.next.interrupt.requested"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "timestamp": {
+ "type": "number"
+ },
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["timestamp", "sessionID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"EventSessionNextContextUpdated": {
"type": "object",
"properties": {
@@ -35984,51 +38089,8 @@
"type": "string",
"enum": ["file"]
},
- "source": {
- "anyOf": [
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["data"]
- },
- "data": {
- "type": "string"
- }
- },
- "required": ["type", "data"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["url"]
- },
- "url": {
- "type": "string"
- }
- },
- "required": ["type", "url"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["file"]
- },
- "uri": {
- "type": "string"
- }
- },
- "required": ["type", "uri"],
- "additionalProperties": false
- }
- ]
+ "uri": {
+ "type": "string"
},
"mime": {
"type": "string"
@@ -36037,7 +38099,7 @@
"type": "string"
}
},
- "required": ["type", "source", "mime"],
+ "required": ["type", "uri", "mime"],
"additionalProperties": false
},
"EventSessionNextToolProgress": {
@@ -36134,6 +38196,12 @@
]
}
},
+ "outputPaths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"result": {},
"provider": {
"type": "object",
@@ -36335,11 +38403,15 @@
"type": "string",
"pattern": "^ses"
},
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg_"
+ },
"text": {
"type": "string"
}
},
- "required": ["timestamp", "sessionID", "text"],
+ "required": ["timestamp", "sessionID", "messageID", "text"],
"additionalProperties": false
}
},
@@ -36366,9 +38438,20 @@
"type": "string",
"pattern": "^ses"
},
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg_"
+ },
+ "reason": {
+ "type": "string",
+ "enum": ["auto", "manual"]
+ },
"text": {
"type": "string"
},
+ "recent": {
+ "type": "string"
+ },
"include": {
"type": "string"
}
@@ -36644,6 +38727,9 @@
{
"$ref": "#/components/schemas/ContextOverflowError"
},
+ {
+ "$ref": "#/components/schemas/ContentFilterError"
+ },
{
"$ref": "#/components/schemas/APIError"
},
@@ -36818,26 +38904,7 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
- "SessionTodoInfo": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string",
- "description": "Brief description of the task"
- },
- "status": {
- "type": "string",
- "description": "Current status of the task: pending, in_progress, completed, cancelled"
- },
- "priority": {
- "type": "string",
- "description": "Priority level of the task: high, medium, low"
- }
- },
- "required": ["content", "status", "priority"],
- "additionalProperties": false
- },
- "EventTodoUpdated": {
+ "EventReferenceUpdated": {
"type": "object",
"properties": {
"id": {
@@ -36845,259 +38912,7 @@
},
"type": {
"type": "string",
- "enum": ["todo.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- },
- "todos": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/SessionTodoInfo"
- }
- }
- },
- "required": ["sessionID", "todos"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventSessionStatus": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["session.status"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- },
- "status": {
- "$ref": "#/components/schemas/SessionStatus"
- }
- },
- "required": ["sessionID", "status"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventSessionIdle": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["session.idle"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- }
- },
- "required": ["sessionID"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventSessionCompacted": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["session.compacted"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- }
- },
- "required": ["sessionID"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventCommandExecuted": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["command.executed"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- },
- "arguments": {
- "type": "string"
- },
- "messageID": {
- "type": "string",
- "pattern": "^msg"
- }
- },
- "required": ["name", "sessionID", "arguments", "messageID"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventProjectDirectoriesUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["project.directories.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "projectID": {
- "type": "string"
- }
- },
- "required": ["projectID"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventProjectUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["project.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "worktree": {
- "type": "string"
- },
- "vcs": {
- "type": "string",
- "enum": ["git"]
- },
- "name": {
- "type": "string"
- },
- "icon": {
- "type": "object",
- "properties": {
- "url": {
- "type": "string"
- },
- "override": {
- "type": "string"
- },
- "color": {
- "type": "string"
- }
- },
- "additionalProperties": false
- },
- "commands": {
- "type": "object",
- "properties": {
- "start": {
- "type": "string",
- "description": "Startup script to run when creating a new workspace (worktree)"
- }
- },
- "additionalProperties": false
- },
- "time": {
- "type": "object",
- "properties": {
- "created": {
- "type": "integer",
- "minimum": 0
- },
- "updated": {
- "type": "integer",
- "minimum": 0
- },
- "initialized": {
- "type": "integer",
- "minimum": 0
- }
- },
- "required": ["created", "updated"],
- "additionalProperties": false
- },
- "sandboxes": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["id", "worktree", "time", "sandboxes"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventLspUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["lsp.updated"]
+ "enum": ["reference.updated"]
},
"properties": {
"type": "object",
@@ -37107,7 +38922,7 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
- "EventFileEdited": {
+ "EventIntegrationUpdated": {
"type": "object",
"properties": {
"id": {
@@ -37115,347 +38930,11 @@
},
"type": {
"type": "string",
- "enum": ["file.edited"]
+ "enum": ["integration.updated"]
},
"properties": {
"type": "object",
- "properties": {
- "file": {
- "type": "string"
- }
- },
- "required": ["file"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventFileWatcherUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["file.watcher.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "file": {
- "type": "string"
- },
- "event": {
- "type": "string",
- "enum": ["add", "change", "unlink"]
- }
- },
- "required": ["file", "event"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventVcsBranchUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["vcs.branch.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "branch": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventWorkspaceReady": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["workspace.ready"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- }
- },
- "required": ["name"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventWorkspaceFailed": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["workspace.failed"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": ["message"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventWorkspaceStatus": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["workspace.status"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "workspaceID": {
- "type": "string",
- "pattern": "^wrk"
- },
- "status": {
- "type": "string",
- "enum": ["connected", "connecting", "disconnected", "error"]
- }
- },
- "required": ["workspaceID", "status"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventWorktreeReady": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["worktree.ready"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "branch": {
- "type": "string"
- }
- },
- "required": ["name"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventWorktreeFailed": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["worktree.failed"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": ["message"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "AuthOAuthCredential": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["oauth"]
- },
- "refresh": {
- "type": "string"
- },
- "access": {
- "type": "string"
- },
- "expires": {
- "type": "integer",
- "minimum": 0
- },
- "accountId": {
- "type": "string"
- }
- },
- "required": ["type", "refresh", "access", "expires"],
- "additionalProperties": false
- },
- "AuthApiKeyCredential": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["api"]
- },
- "key": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "type": "string"
- }
- }
- },
- "required": ["type", "key"],
- "additionalProperties": false
- },
- "AuthCredential": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/AuthOAuthCredential"
- },
- {
- "$ref": "#/components/schemas/AuthApiKeyCredential"
- }
- ]
- },
- "AuthInfo": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "serviceID": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "credential": {
- "$ref": "#/components/schemas/AuthCredential"
- }
- },
- "required": ["id", "serviceID", "description", "credential"],
- "additionalProperties": false
- },
- "EventAccountAdded": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["account.added"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "account": {
- "$ref": "#/components/schemas/AuthInfo"
- }
- },
- "required": ["account"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventAccountRemoved": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["account.removed"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "account": {
- "$ref": "#/components/schemas/AuthInfo"
- }
- },
- "required": ["account"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventAccountSwitched": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["account.switched"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "serviceID": {
- "type": "string"
- },
- "from": {
- "type": "string"
- },
- "to": {
- "type": "string"
- }
- },
- "required": ["serviceID"],
- "additionalProperties": false
+ "properties": {}
}
},
"required": ["id", "type", "properties"],
@@ -37564,6 +39043,82 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventProjectDirectoriesUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["project.directories.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "projectID": {
+ "type": "string"
+ }
+ },
+ "required": ["projectID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventFileEdited": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["file.edited"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string"
+ }
+ },
+ "required": ["file"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventFileWatcherUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["file.watcher.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string"
+ },
+ "event": {
+ "type": "string",
+ "enum": ["add", "change", "unlink"]
+ }
+ },
+ "required": ["file", "event"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"EventPtyCreated": {
"type": "object",
"properties": {
@@ -37831,6 +39386,403 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventTodoUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["todo.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "todos": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "required": ["sessionID", "todos"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventSessionStatus": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.status"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "status": {
+ "$ref": "#/components/schemas/SessionStatus"
+ }
+ },
+ "required": ["sessionID", "status"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventSessionIdle": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.idle"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["sessionID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventSessionCompacted": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.compacted"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["sessionID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventCommandExecuted": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["command.executed"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "arguments": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg"
+ }
+ },
+ "required": ["name", "sessionID", "arguments", "messageID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventProjectUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["project.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "worktree": {
+ "type": "string"
+ },
+ "vcs": {
+ "type": "string",
+ "enum": ["git"]
+ },
+ "name": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "override": {
+ "type": "string"
+ },
+ "color": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "commands": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "string",
+ "description": "Startup script to run when creating a new workspace (worktree)"
+ }
+ },
+ "additionalProperties": false
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "updated": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "initialized": {
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "required": ["created", "updated"],
+ "additionalProperties": false
+ },
+ "sandboxes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["id", "worktree", "time", "sandboxes"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventLspUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["lsp.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventVcsBranchUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["vcs.branch.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "branch": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventWorkspaceReady": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["workspace.ready"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["name"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventWorkspaceFailed": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["workspace.failed"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventWorkspaceStatus": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["workspace.status"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "workspaceID": {
+ "type": "string",
+ "pattern": "^wrk"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["connected", "connecting", "disconnected", "error"]
+ }
+ },
+ "required": ["workspaceID", "status"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventWorktreeReady": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["worktree.ready"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ }
+ },
+ "required": ["name"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventWorktreeFailed": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["worktree.failed"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"SyncEventSessionCreated": {
"type": "object",
"properties": {
@@ -39589,6 +41541,12 @@
]
}
},
+ "outputPaths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"result": {},
"provider": {
"type": "object",
@@ -39817,59 +41775,6 @@
"required": ["type", "id", "syncEvent"],
"additionalProperties": false
},
- "SyncEventSessionNextCompactionDelta": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["sync"]
- },
- "id": {
- "type": "string",
- "pattern": "^evt_"
- },
- "syncEvent": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["session.next.compaction.delta.1"]
- },
- "id": {
- "type": "string",
- "pattern": "^evt_"
- },
- "seq": {
- "type": "number"
- },
- "aggregateID": {
- "type": "string"
- },
- "data": {
- "type": "object",
- "properties": {
- "timestamp": {
- "type": "number"
- },
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- },
- "text": {
- "type": "string"
- }
- },
- "required": ["timestamp", "sessionID", "text"],
- "additionalProperties": false
- }
- },
- "required": ["type", "id", "seq", "aggregateID", "data"],
- "additionalProperties": false
- }
- },
- "required": ["type", "id", "syncEvent"],
- "additionalProperties": false
- },
"SyncEventSessionNextCompactionEnded": {
"type": "object",
"properties": {
@@ -39908,9 +41813,20 @@
"type": "string",
"pattern": "^ses"
},
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg_"
+ },
+ "reason": {
+ "type": "string",
+ "enum": ["auto", "manual"]
+ },
"text": {
"type": "string"
},
+ "recent": {
+ "type": "string"
+ },
"include": {
"type": "string"
}
@@ -39926,6 +41842,41 @@
"required": ["type", "id", "syncEvent"],
"additionalProperties": false
},
+ "ConfigV2ReferenceGit": {
+ "type": "object",
+ "properties": {
+ "repository": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ }
+ },
+ "required": ["repository"],
+ "additionalProperties": false
+ },
+ "ConfigV2ReferenceLocal": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ }
+ },
+ "required": ["path"],
+ "additionalProperties": false
+ },
"PolicyEffect": {
"type": "string",
"enum": ["allow", "deny"]
@@ -39950,19 +41901,19 @@
"ProjectDirectories": {
"type": "array",
"items": {
- "type": "string"
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "strategy": {
+ "type": "string"
+ }
+ },
+ "required": ["directory"],
+ "additionalProperties": false
}
},
- "ProjectCopyCopy": {
- "type": "object",
- "properties": {
- "directory": {
- "type": "string"
- }
- },
- "required": ["directory"],
- "additionalProperties": false
- },
"LocationInfo": {
"type": "object",
"properties": {
@@ -40327,12 +42278,6 @@
"$ref": "#/components/schemas/PromptAgentAttachment"
}
},
- "references": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/PromptReferenceAttachment"
- }
- },
"type": {
"type": "string",
"enum": ["user"]
@@ -40560,6 +42505,12 @@
]
}
},
+ "outputPaths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"structured": {
"type": "object"
},
@@ -40806,7 +42757,7 @@
"summary": {
"type": "string"
},
- "include": {
+ "recent": {
"type": "string"
},
"id": {
@@ -40827,7 +42778,7 @@
"additionalProperties": false
}
},
- "required": ["type", "reason", "summary", "id", "time"],
+ "required": ["type", "reason", "summary", "recent", "id", "time"],
"additionalProperties": false
},
"SessionMessage": {
@@ -40892,13 +42843,13 @@
"properties": {
"via": {
"type": "string",
- "enum": ["account"]
+ "enum": ["credential"]
},
- "service": {
+ "credentialID": {
"type": "string"
}
},
- "required": ["via", "service"],
+ "required": ["via", "credentialID"],
"additionalProperties": false
},
{
@@ -40984,6 +42935,295 @@
"required": ["id", "name", "enabled", "env", "api", "request"],
"additionalProperties": false
},
+ "IntegrationWhen": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "op": {
+ "type": "string",
+ "enum": ["eq", "neq"]
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": ["key", "op", "value"],
+ "additionalProperties": false
+ },
+ "IntegrationTextPrompt": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["text"]
+ },
+ "key": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "when": {
+ "$ref": "#/components/schemas/IntegrationWhen"
+ }
+ },
+ "required": ["type", "key", "message"],
+ "additionalProperties": false
+ },
+ "IntegrationSelectPrompt": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["select"]
+ },
+ "key": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ }
+ },
+ "required": ["label", "value"],
+ "additionalProperties": false
+ }
+ },
+ "when": {
+ "$ref": "#/components/schemas/IntegrationWhen"
+ }
+ },
+ "required": ["type", "key", "message", "options"],
+ "additionalProperties": false
+ },
+ "IntegrationOAuthMethod": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["oauth"]
+ },
+ "label": {
+ "type": "string"
+ },
+ "prompts": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/IntegrationTextPrompt"
+ },
+ {
+ "$ref": "#/components/schemas/IntegrationSelectPrompt"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["id", "type", "label"],
+ "additionalProperties": false
+ },
+ "IntegrationKeyMethod": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["key"]
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false
+ },
+ "IntegrationEnvMethod": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["env"]
+ },
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["type", "names"],
+ "additionalProperties": false
+ },
+ "ConnectionCredentialInfo": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["credential"]
+ },
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "id", "label"],
+ "additionalProperties": false
+ },
+ "ConnectionEnvInfo": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["env"]
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "name"],
+ "additionalProperties": false
+ },
+ "ConnectionInfo": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ConnectionCredentialInfo"
+ },
+ {
+ "$ref": "#/components/schemas/ConnectionEnvInfo"
+ }
+ ]
+ },
+ "IntegrationInfo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "methods": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/IntegrationOAuthMethod"
+ },
+ {
+ "$ref": "#/components/schemas/IntegrationKeyMethod"
+ },
+ {
+ "$ref": "#/components/schemas/IntegrationEnvMethod"
+ }
+ ]
+ }
+ },
+ "connections": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ConnectionInfo"
+ }
+ }
+ },
+ "required": ["id", "name", "methods", "connections"],
+ "additionalProperties": false
+ },
+ "IntegrationAttempt": {
+ "type": "object",
+ "properties": {
+ "attemptID": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "instructions": {
+ "type": "string"
+ },
+ "mode": {
+ "type": "string",
+ "enum": ["auto", "code"]
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ },
+ "expires": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity", "-Infinity", "NaN"]
+ }
+ ]
+ }
+ },
+ "required": ["created", "expires"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["attemptID", "url", "instructions", "mode", "time"],
+ "additionalProperties": false
+ },
"PermissionV2Request": {
"type": "object",
"properties": {
@@ -41039,53 +43279,12 @@
"required": ["id", "projectID", "action", "resource"],
"additionalProperties": false
},
- "FileSystemTextContent": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["text"]
- },
- "content": {
- "type": "string"
- },
- "mime": {
- "type": "string"
- }
- },
- "required": ["type", "content", "mime"],
- "additionalProperties": false
- },
- "FileSystemBinaryContent": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["binary"]
- },
- "content": {
- "type": "string"
- },
- "encoding": {
- "type": "string",
- "enum": ["base64"]
- },
- "mime": {
- "type": "string"
- }
- },
- "required": ["type", "content", "encoding", "mime"],
- "additionalProperties": false
- },
"FileSystemEntry": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
- "uri": {
- "type": "string"
- },
"type": {
"type": "string",
"enum": ["file", "directory"]
@@ -41094,7 +43293,7 @@
"type": "string"
}
},
- "required": ["path", "uri", "type", "mime"],
+ "required": ["path", "type", "mime"],
"additionalProperties": false
},
"CommandV2Info": {
@@ -41196,6 +43395,88 @@
"required": ["answers"],
"additionalProperties": false
},
+ "ReferenceLocalSource": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["local"]
+ },
+ "path": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ }
+ },
+ "required": ["type", "path"],
+ "additionalProperties": false
+ },
+ "ReferenceGitSource": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["git"]
+ },
+ "repository": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ }
+ },
+ "required": ["type", "repository"],
+ "additionalProperties": false
+ },
+ "ReferenceInfo": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "source": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceLocalSource"
+ },
+ {
+ "$ref": "#/components/schemas/ReferenceGitSource"
+ }
+ ]
+ }
+ },
+ "required": ["name", "path", "source"],
+ "additionalProperties": false
+ },
+ "ProjectCopyCopy": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["directory"],
+ "additionalProperties": false
+ },
"EventMemoryStatus1": {
"type": "object",
"properties": {
@@ -42189,6 +44470,154 @@
"body": {
"type": "object"
},
+ "generation": {
+ "type": "object",
+ "properties": {
+ "maxTokens": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "temperature": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "topP": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "topK": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "frequencyPenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "presencePenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "seed": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "stop": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "options": {
+ "type": "object"
+ },
"variant": {
"type": "string"
}
@@ -42212,6 +44641,154 @@
},
"body": {
"type": "object"
+ },
+ "generation": {
+ "type": "object",
+ "properties": {
+ "maxTokens": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "temperature": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "topP": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "topK": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "frequencyPenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "presencePenalty": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "seed": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": ["NaN"]
+ },
+ {
+ "type": "string",
+ "enum": ["Infinity"]
+ },
+ {
+ "type": "string",
+ "enum": ["-Infinity"]
+ }
+ ]
+ },
+ "stop": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "options": {
+ "type": "object"
}
},
"required": ["id", "headers", "body"],
@@ -42431,7 +45008,7 @@
},
{
"name": "projectCopy",
- "description": "Project copy management routes."
+ "description": "Project copy naming routes."
},
{
"name": "pty",
@@ -42542,64 +45119,72 @@
"description": "Kilo memory routes."
},
{
- "name": "kilo experimental HttpApi",
+ "name": "Kilo HttpApi",
"description": "Experimental HttpApi surface for selected instance routes."
},
{
- "name": "kilo experimental HttpApi",
+ "name": "Kilo HttpApi",
"description": "Experimental HttpApi surface for selected instance routes."
},
{
- "name": "v2",
- "description": "Experimental v2 routes."
+ "name": "Kilo HttpApi",
+ "description": "Experimental HttpApi surface for selected instance routes."
},
{
- "name": "v2 messages",
- "description": "Experimental v2 message routes."
+ "name": "sessions",
+ "description": "Experimental session routes."
},
{
- "name": "v2 models",
- "description": "Experimental v2 model routes."
+ "name": "messages",
+ "description": "Experimental message routes."
},
{
- "name": "v2 providers",
- "description": "Experimental v2 provider routes."
+ "name": "models",
+ "description": "Experimental model routes."
},
{
- "name": "v2 permissions",
- "description": "Experimental v2 permission routes."
+ "name": "providers",
+ "description": "Experimental provider routes."
},
{
- "name": "v2 session permissions",
- "description": "Experimental v2 session permission routes."
+ "name": "integrations",
+ "description": "Integration discovery and authentication routes."
},
{
- "name": "v2 saved permissions",
- "description": "Experimental v2 saved permission routes."
+ "name": "Kilo HttpApi",
+ "description": "Experimental HttpApi surface for selected instance routes."
},
{
- "name": "v2 filesystem",
- "description": "Experimental v2 location-scoped filesystem routes."
+ "name": "permissions",
+ "description": "Experimental permission routes."
},
{
- "name": "v2 commands",
- "description": "Experimental v2 command routes."
+ "name": "filesystem",
+ "description": "Experimental location-scoped filesystem routes."
},
{
- "name": "v2 skills",
- "description": "Experimental v2 skill routes."
+ "name": "commands",
+ "description": "Experimental command routes."
},
{
- "name": "v2 events",
- "description": "Experimental v2 event stream route."
+ "name": "skills",
+ "description": "Experimental skill routes."
},
{
- "name": "v2 questions",
- "description": "Experimental v2 question routes."
+ "name": "events",
+ "description": "Experimental event stream route."
},
{
- "name": "v2 session questions",
- "description": "Experimental v2 session question routes."
+ "name": "session questions",
+ "description": "Experimental session question routes."
+ },
+ {
+ "name": "reference",
+ "description": "Location-scoped project references."
+ },
+ {
+ "name": "projectCopy",
+ "description": "Project copy management routes."
},
{
"name": "pty",
From b71a9c0c234cb2fbb55712e4e995e261dde9fab5 Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Mon, 27 Jul 2026 22:10:56 -0400
Subject: [PATCH 09/71] fix(vscode): include web search in config exports
---
.../webview-ui/src/components/settings/settings-io.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts
index d4b1949d2a9..be43b46284a 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts
@@ -34,6 +34,7 @@ export const KNOWN_KEYS: ReadonlyArray = [
"compaction",
"commit_message",
"tools",
+ "web_search",
"auto_collapse_reasoning",
"terminal_command_display",
"code_edit_display",
From 2dba76605c6234f2e0f13bd1e475daed5b332a44 Mon Sep 17 00:00:00 2001
From: Josh Lambert
Date: Tue, 28 Jul 2026 06:52:48 -0400
Subject: [PATCH 10/71] docs: fix typo in NVIDIA BYOK note
---
packages/kilo-docs/pages/getting-started/byok.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md
index 43f00eac318..55b27425448 100644
--- a/packages/kilo-docs/pages/getting-started/byok.md
+++ b/packages/kilo-docs/pages/getting-started/byok.md
@@ -92,7 +92,7 @@ Your IAM user or role must have the following permissions:
Create an API key in the [NVIDIA API Catalog](https://build.nvidia.com/settings/api-keys), then add it as the NVIDIA provider.
-Models that do not support tool calling are excluded, as they are unlikely to be provide useful results.
+Models that do not support tool calling are excluded, as they are unlikely to provide useful results.
{% callout type="warning" title="NVIDIA API Catalog terms" %}
NVIDIA limits Developer Program endpoints to prototyping, research, development, and testing. Serving production end users may require NVIDIA AI Enterprise licensing. Your prompts and model outputs are sent to NVIDIA under your NVIDIA agreement. Review the [API Catalog quickstart](https://docs.api.nvidia.com/nim/docs/api-quickstart) and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
From b0a546049e7fcb212c8d1db344a48531ce65bed9 Mon Sep 17 00:00:00 2001
From: marius-kilocode
Date: Tue, 28 Jul 2026 13:42:15 +0200
Subject: [PATCH 11/71] refactor(cli): remove provably unused kilocode code
Delete code with zero references anywhere in the monorepo (including
tests, docs, scripts, and dynamic import or string-based usage):
- background-process/windows-job.ts: entire Windows Job Object FFI
module left unreferenced after the background process runner refactor
- session/prompt.ts: createShellDecoders helper, CODE_SWITCH_TEXT alias,
and the now-unused StringDecoder import
- text-stream.ts: openUtf8 wrapper
- cli/cmd/tui/component/prompt/vim.ts: enterInsert helper
- config/config.ts: KILO_CONFIG_FILES, AGENT_PATTERNS, COMMAND_PATTERNS
- remote-attachments.ts: TEXT_PLAIN constant
- plan-followup.ts: PLAN_PREFIX constant
- server/httpapi/groups/background-process.ts: SessionParams schema
- server/httpapi/groups/session-import.ts: SessionImportPayloads map
- cloud/contracts.ts: RepositoryInput type alias
---
.../background-process/windows-job.ts | 88 -------------------
.../cli/cmd/tui/component/prompt/vim.ts | 5 --
.../opencode/src/kilocode/cloud/contracts.ts | 1 -
.../opencode/src/kilocode/config/config.ts | 14 ---
.../opencode/src/kilocode/plan-followup.ts | 1 -
.../src/kilocode/remote-attachments.ts | 1 -
.../httpapi/groups/background-process.ts | 1 -
.../server/httpapi/groups/session-import.ts | 7 --
.../opencode/src/kilocode/session/prompt.ts | 26 ------
packages/opencode/src/kilocode/text-stream.ts | 5 --
10 files changed, 149 deletions(-)
delete mode 100644 packages/opencode/src/kilocode/background-process/windows-job.ts
diff --git a/packages/opencode/src/kilocode/background-process/windows-job.ts b/packages/opencode/src/kilocode/background-process/windows-job.ts
deleted file mode 100644
index 85d05b38e6f..00000000000
--- a/packages/opencode/src/kilocode/background-process/windows-job.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { dlopen, ptr } from "bun:ffi"
-
-export namespace WindowsJob {
- const LIMITS = 9
- const MEMBERS = 3
- const KILL_ON_CLOSE = 0x00002000
- const PROCESS_TERMINATE = 0x0001
- const PROCESS_SET_QUOTA = 0x0100
- const MORE_DATA = 234
-
- function kernel() {
- return dlopen("kernel32.dll", {
- CreateJobObjectW: { args: ["ptr", "ptr"], returns: "u64" },
- SetInformationJobObject: { args: ["u64", "u32", "ptr", "u32"], returns: "i32" },
- OpenProcess: { args: ["u32", "i32", "u32"], returns: "u64" },
- AssignProcessToJobObject: { args: ["u64", "u64"], returns: "i32" },
- QueryInformationJobObject: { args: ["u64", "u32", "ptr", "u32", "ptr"], returns: "i32" },
- TerminateJobObject: { args: ["u64", "u32"], returns: "i32" },
- CloseHandle: { args: ["u64"], returns: "i32" },
- GetLastError: { args: [], returns: "u32" },
- })
- }
-
- export function create() {
- const lib = (() => {
- try {
- return kernel()
- } catch {
- return undefined
- }
- })()
- if (!lib) return
- const handle = lib.symbols.CreateJobObjectW(null, null)
- if (handle === 0n) {
- const code = lib.symbols.GetLastError()
- lib.close()
- throw new Error(`CreateJobObjectW failed with Windows error ${code}`)
- }
- const limits = new Uint8Array(144)
- new DataView(limits.buffer).setUint32(16, KILL_ON_CLOSE, true)
- if (lib.symbols.SetInformationJobObject(handle, LIMITS, ptr(limits), limits.byteLength) === 0) {
- const code = lib.symbols.GetLastError()
- lib.symbols.CloseHandle(handle)
- lib.close()
- throw new Error(`SetInformationJobObject failed with Windows error ${code}`)
- }
- let closed = false
- return {
- assign(pid: number) {
- const proc = lib.symbols.OpenProcess(PROCESS_TERMINATE | PROCESS_SET_QUOTA, 0, pid)
- if (proc === 0n) throw new Error(`OpenProcess failed with Windows error ${lib.symbols.GetLastError()}`)
- const assigned = lib.symbols.AssignProcessToJobObject(handle, proc)
- const code = assigned === 0 ? lib.symbols.GetLastError() : 0
- lib.symbols.CloseHandle(proc)
- if (assigned === 0) throw new Error(`AssignProcessToJobObject failed with Windows error ${code}`)
- },
- members() {
- let size = 4 * 1024
- while (true) {
- const info = new Uint8Array(size)
- const ok = lib.symbols.QueryInformationJobObject(handle, MEMBERS, ptr(info), info.byteLength, null)
- const code = ok === 0 ? lib.symbols.GetLastError() : 0
- const view = new DataView(info.buffer)
- const assigned = view.getUint32(0, true)
- const count = view.getUint32(4, true)
- if (ok !== 0 && count === assigned) {
- return Array.from({ length: count }, (_, index) => Number(view.getBigUint64(8 + index * 8, true)))
- }
- if (ok === 0 && code !== MORE_DATA) {
- throw new Error(`QueryInformationJobObject failed with Windows error ${code}`)
- }
- size = Math.max(size * 2, 8 + assigned * 8)
- }
- },
- terminate() {
- if (lib.symbols.TerminateJobObject(handle, 1) === 0) {
- throw new Error(`TerminateJobObject failed with Windows error ${lib.symbols.GetLastError()}`)
- }
- },
- close() {
- if (closed) return
- closed = true
- lib.symbols.CloseHandle(handle)
- lib.close()
- },
- }
- }
-}
diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts
index 2b91606c86c..020d0be3793 100644
--- a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts
+++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts
@@ -877,8 +877,3 @@ export function handleVisualKey(doc: VimDoc, state: VimState, input: VimKey): Vi
state.countDigits = ""
return { handled: true }
}
-
-export function enterInsert(state: VimState) {
- state.mode = "insert"
- resetPending(state)
-}
diff --git a/packages/opencode/src/kilocode/cloud/contracts.ts b/packages/opencode/src/kilocode/cloud/contracts.ts
index a4716e547eb..a78b846b8cb 100644
--- a/packages/opencode/src/kilocode/cloud/contracts.ts
+++ b/packages/opencode/src/kilocode/cloud/contracts.ts
@@ -163,7 +163,6 @@ export const GetMessageResultOutputSchema = z
}
})
-export type RepositoryInput = z.infer
export type AgentStartRequest = z.infer
export type AgentSendRequest = z.infer
export type GetMessageResultInput = z.infer
diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts
index a33445494b8..608386d4fad 100644
--- a/packages/opencode/src/kilocode/config/config.ts
+++ b/packages/opencode/src/kilocode/config/config.ts
@@ -37,26 +37,12 @@ export namespace KilocodeConfig {
// ── Config file constants ────────────────────────────────────────────
- /** Kilo-specific config file names (highest-to-lowest precedence within kilo). */
- export const KILO_CONFIG_FILES = ["kilo.jsonc", "kilo.json"] as const
-
/** All config file names in precedence order (kilo + opencode). */
export const ALL_CONFIG_FILES = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const
/** Config directory suffixes in update-target preference order. */
export const KILO_DIR_SUFFIXES = [".kilo", ".kilocode"] as const
- /** Path patterns for resolving kilo agent names from file paths. */
- export const AGENT_PATTERNS = ["/.kilo/agent/", "/.kilo/agents/", "/.kilocode/agent/", "/.kilocode/agents/"] as const
-
- /** Path patterns for resolving kilo command names from file paths. */
- export const COMMAND_PATTERNS = [
- "/.kilo/command/",
- "/.kilo/commands/",
- "/.kilocode/command/",
- "/.kilocode/commands/",
- ] as const
-
/**
* Choose the project config file that Config.update should patch.
*
diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts
index 01de704a546..a0c726f94ca 100644
--- a/packages/opencode/src/kilocode/plan-followup.ts
+++ b/packages/opencode/src/kilocode/plan-followup.ts
@@ -157,7 +157,6 @@ export async function generateHandover(input: {
export namespace PlanFollowup {
const log = Log.create({ service: "plan.followup" })
- export const PLAN_PREFIX = "Implement the following plan:"
export const ANSWER_NEW_SESSION = "Start new session"
export const ANSWER_CONTINUE = "Continue here"
export const ANSWER_KEEP_REFINING = "Keep refining"
diff --git a/packages/opencode/src/kilocode/remote-attachments.ts b/packages/opencode/src/kilocode/remote-attachments.ts
index 9008065cc90..b329dbb47d2 100644
--- a/packages/opencode/src/kilocode/remote-attachments.ts
+++ b/packages/opencode/src/kilocode/remote-attachments.ts
@@ -66,7 +66,6 @@ export namespace RemoteAttachments {
sql: "text/plain",
}
export const BINARY_MIME = "application/octet-stream"
- export const TEXT_PLAIN = "text/plain"
// Hard cap on attachment bytes (5 MB + 1 byte so the helper aborts
// strictly when the body exceeds the agreed ceiling).
export const MAX_BYTES = 5 * 1024 * 1024 + 1
diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts
index e5b0dbc2dc7..208cd1f0215 100644
--- a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts
+++ b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts
@@ -22,7 +22,6 @@ export const BackgroundProcessPaths = {
} as const
export const Params = Schema.Struct({ processID: BackgroundProcess.ID })
-export const SessionParams = Schema.Struct({ sessionID: SessionID })
export const BackgroundProcessApi = HttpApi.make("background-process")
.add(
diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts b/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts
index 596ca4f57d6..9aca9d4581f 100644
--- a/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts
+++ b/packages/opencode/src/kilocode/server/httpapi/groups/session-import.ts
@@ -219,13 +219,6 @@ export const SessionImportPaths = {
part: `${root}/part`,
} as const
-export const SessionImportPayloads = {
- Project: ProjectSchema,
- Session: SessionSchema,
- Message: MessageSchema,
- Part: PartSchema,
-} as const
-
export const SessionImportApi = HttpApi.make("session-import")
.add(
HttpApiGroup.make("session-import")
diff --git a/packages/opencode/src/kilocode/session/prompt.ts b/packages/opencode/src/kilocode/session/prompt.ts
index a1fa29b8064..ee05f453581 100644
--- a/packages/opencode/src/kilocode/session/prompt.ts
+++ b/packages/opencode/src/kilocode/session/prompt.ts
@@ -1,7 +1,6 @@
// kilocode_change - new file
import path from "path"
import fs from "fs/promises"
-import { StringDecoder } from "string_decoder"
import { Cause, Effect, Exit, Fiber, Scope } from "effect"
import { SessionID, PartID } from "@/session/schema"
import { MessageV2 } from "@/session/message-v2"
@@ -377,25 +376,6 @@ export namespace KiloSessionPrompt {
}
}
- /**
- * Creates StringDecoder-based helpers for shell stdout/stderr that correctly
- * handle multi-byte UTF-8 characters split across chunks.
- */
- export function createShellDecoders() {
- const stdout = new StringDecoder("utf8")
- const stderr = new StringDecoder("utf8")
- return {
- /** Decode a chunk from the given stream. */
- write(stream: "stdout" | "stderr", chunk: Buffer) {
- return stream === "stdout" ? stdout.write(chunk) : stderr.write(chunk)
- },
- /** Flush any trailing buffered bytes from both decoders. */
- flush() {
- return stdout.end() + stderr.end()
- },
- }
- }
-
/**
* Ensures the plan file directory exists. Pre-checks with `Filesystem.isDir`
* because `fs.mkdir(recursive: true)` still throws `EEXIST` on Windows
@@ -455,12 +435,6 @@ export namespace KiloSessionPrompt {
add(`\n${body}\n `)
}
- /**
- * Returns the CODE_SWITCH prompt text (plan-to-code transition).
- * Used when switching from plan agent to code agent.
- */
- export const CODE_SWITCH_TEXT = CODE_SWITCH
-
/**
* Determines the close reason for a session turn.
* Checks for an explicit reason first (e.g. set on error during runLoop),
diff --git a/packages/opencode/src/kilocode/text-stream.ts b/packages/opencode/src/kilocode/text-stream.ts
index 82f2f6cd7c2..bfaabd164eb 100644
--- a/packages/opencode/src/kilocode/text-stream.ts
+++ b/packages/opencode/src/kilocode/text-stream.ts
@@ -62,11 +62,6 @@ export function abortable(stream: Readable, signal?: AbortSignal) {
return signal ? addAbortSignal(signal, stream) : stream
}
-/** UTF-8 text stream backed by an already-open file. */
-export function openUtf8(open: () => Readable, signal?: AbortSignal): Readable {
- return utf8(open, signal).stream
-}
-
export function safeSlice(text: string, end: number) {
const sliced = text.slice(0, end)
const last = sliced.charCodeAt(sliced.length - 1)
From c08f37e88b7d063429fba5b164e31c94d1935610 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Tue, 28 Jul 2026 15:26:14 +0200
Subject: [PATCH 12/71] feat(cli): execute shell commands in skill files with
batch approval
---
packages/opencode/src/cli/cmd/run.ts | 6 +
packages/opencode/src/effect/runtime-flags.ts | 1 +
.../opencode/src/kilocode/permission/drain.ts | 2 +
.../opencode/src/kilocode/skills/inject.ts | 78 +++++++
packages/opencode/src/permission/index.ts | 13 ++
packages/opencode/src/skill/index.ts | 3 +
packages/opencode/src/tool/skill.ts | 21 +-
.../kilocode/permission/skill-shell.test.ts | 127 +++++++++++
.../test/kilocode/skills/inject.test.ts | 197 ++++++++++++++++++
.../tui/src/routes/session/permission.tsx | 24 ++-
10 files changed, 466 insertions(+), 6 deletions(-)
create mode 100644 packages/opencode/src/kilocode/skills/inject.ts
create mode 100644 packages/opencode/test/kilocode/permission/skill-shell.test.ts
create mode 100644 packages/opencode/test/kilocode/skills/inject.test.ts
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 56241ace1a1..d77916109ed 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -840,6 +840,12 @@ export const RunCommand = effectCmd({
if (event.type === "permission.asked") {
const permission = event.properties
+ // kilocode_change start - skill shell batches need an interactive human decision; never headless auto-approve
+ if (permission.metadata?.["skillShell"] === true) {
+ await client.permission.reply({ requestID: permission.id, reply: "reject" })
+ continue
+ }
+ // kilocode_change end
// kilocode_change start - approve root and tracked Task child permissions in auto mode
if (args.auto) {
if (!KiloRunAuto.allowed(auto, permission.sessionID)) continue
diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts
index 188dc4869a0..f1bf3edb26c 100644
--- a/packages/opencode/src/effect/runtime-flags.ts
+++ b/packages/opencode/src/effect/runtime-flags.ts
@@ -20,6 +20,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime
disableChannelDb: bool("KILO_DISABLE_CHANNEL_DB"), // kilocode_change
disableEmbeddedWebUi: bool("KILO_DISABLE_EMBEDDED_WEB_UI"),
disableExternalSkills: bool("KILO_DISABLE_EXTERNAL_SKILLS"),
+ disableSkillShell: bool("KILO_DISABLE_SKILL_SHELL"), // kilocode_change - disable shell injection in skill bodies
disableLspDownload: bool("KILO_DISABLE_LSP_DOWNLOAD"),
skipMigrations: bool("KILO_SKIP_MIGRATIONS"), // kilocode_change
disableClaudeCodePrompt: Config.all({
diff --git a/packages/opencode/src/kilocode/permission/drain.ts b/packages/opencode/src/kilocode/permission/drain.ts
index 78f960c867e..499f4f83e1e 100644
--- a/packages/opencode/src/kilocode/permission/drain.ts
+++ b/packages/opencode/src/kilocode/permission/drain.ts
@@ -33,6 +33,8 @@ export function drainCovered(
// Never auto-resolve config file edit permissions
const skill = ConfigProtection.globalSkillPattern(entry.info)
if (ConfigProtection.isRequest(entry.info) && !skill) continue
+ // Never auto-resolve a skill shell batch; it must get an explicit reply.
+ if (entry.info.metadata?.["skillShell"] === true) continue
const actions = entry.info.patterns.map((pattern: string) => {
const rule = skill
? Permission.evaluate(entry.info.permission, skill, approved)
diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts
new file mode 100644
index 00000000000..ceee78c7427
--- /dev/null
+++ b/packages/opencode/src/kilocode/skills/inject.ts
@@ -0,0 +1,78 @@
+import { Effect } from "effect"
+import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
+import type { ChildProcessSpawner as Spawner } from "effect/unstable/process/ChildProcessSpawner"
+import { ConfigMarkdown } from "@/config/markdown"
+import { CommandTimeout } from "@/kilocode/command-timeout"
+import { Shell } from "@opencode-ai/core/shell"
+import type * as Tool from "@/tool/tool"
+
+// Shell injection for skill bodies mirrors Claude's "dynamic context injection":
+// a `!`cmd`` placeholder in SKILL.md is replaced by the command's stdout before
+// the content reaches the model. Unlike the slash-command path, this runs for
+// model-initiated skill loads, so it is gated on three independent controls:
+//
+// 1. Trust: only skills from trusted sources (global ~/.claude, ~/.agents,
+// KILO_CONFIG_DIR, and builtins) may execute. Untrusted project/downloaded
+// skills never spawn a process.
+// 2. Kill-switch: `disabled` (KILO_DISABLE_SKILL_SHELL) turns injection off
+// entirely, matching Claude's disableSkillShellExecution.
+// 3. Batch approval: every command in the file is presented once, up front, in
+// a single permission prompt (the `skillShell` metadata marker forces this
+// prompt regardless of any allow/deny/auto-approve rule). Approve runs the
+// whole batch; reject aborts the skill load with nothing run.
+//
+// Substitution runs exactly once. Command output is inlined as plain text and is
+// never re-scanned, so a command cannot emit a `!`cmd`` placeholder that a later
+// pass would execute (second-order injection).
+
+const DISABLED_NOTE = "[skill shell execution disabled by policy]"
+const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]"
+
+export namespace SkillInject {
+ export type Options = {
+ content: string
+ trusted: boolean
+ disabled: boolean
+ ctx: Tool.Context
+ spawner: Spawner["Service"]
+ }
+
+ export const render = Effect.fn("SkillInject.render")(function* (opts: Options) {
+ const matches = ConfigMarkdown.shell(opts.content)
+ if (matches.length === 0) return opts.content
+
+ // Defense-in-depth ordering: policy checks first, approval gate last.
+ if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE)
+ if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE)
+
+ // Deduplicate identical commands so the batch lists and runs each once.
+ const commands = Array.from(new Set(matches.map(([, cmd]) => cmd)))
+
+ // Single up-front approval for the whole batch. `skillShell` forces one
+ // prompt even when rules would allow or deny; a reject/deny propagates as a
+ // defect and aborts the skill load without running anything.
+ yield* opts.ctx.ask({
+ permission: "bash",
+ patterns: commands,
+ always: [],
+ metadata: { skillShell: true },
+ })
+
+ const shell = Shell.preferred()
+ const outputs = new Map()
+ for (const command of commands) {
+ outputs.set(
+ command,
+ yield* CommandTimeout.text(command, shell).pipe(Effect.provideService(ChildProcessSpawner, opts.spawner)),
+ )
+ }
+
+ return replace(opts.content, (command) => outputs.get(command) ?? "")
+ })
+
+ // Replace only the exact matches found in the ORIGINAL content. Never re-scan
+ // the result, so inlined output containing `!`cmd`` stays inert.
+ function replace(content: string, value: (command: string) => string) {
+ return content.replace(ConfigMarkdown.SHELL_REGEX, (_, command: string) => value(command))
+ }
+}
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index f15feff5632..d5217693d20 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -155,6 +155,7 @@ function subset(permission: string, ruleset: Ruleset) {
function covered(entry: PendingEntry, approved: Ruleset, local: Ruleset) {
if (ConfigProtection.isRequest(entry.info)) return false
+ if (entry.info.metadata?.["skillShell"] === true) return false // kilocode_change - skill batch needs an explicit reply
return entry.info.patterns.every((pattern) => {
if (veto(entry.info.permission, pattern, entry.hardRuleset)) return false
return resolve(entry.info.permission, pattern, entry.ruleset, approved, local).action === "allow"
@@ -221,7 +222,19 @@ export const layer = Layer.effect(
: false
// kilocode_change end
+ // kilocode_change start - skill shell injection always prompts once, overriding every allow/deny/auto-approve rule
+ const forceAsk = request.metadata?.["skillShell"] === true
+ // kilocode_change end
for (const pattern of request.patterns) {
+ // kilocode_change start - force a prompt over soft allow/deny rules, but never over a hard (plan-mode) veto
+ if (forceAsk) {
+ if (veto(request.permission, pattern, hardRuleset)) {
+ return yield* new DeniedError({ ruleset: subset(request.permission, hardRuleset ?? []) })
+ }
+ needsAsk = true
+ continue
+ }
+ // kilocode_change end
const rule = resolve(request.permission, pattern, ruleset, approved, local) // kilocode_change — include session-scoped rules
yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: rule })
// kilocode_change start — saved/session approvals cannot override hard Ask/Plan denials
diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts
index 2477093d9fb..1b5f4241cc2 100644
--- a/packages/opencode/src/skill/index.ts
+++ b/packages/opencode/src/skill/index.ts
@@ -35,6 +35,7 @@ export const Info = Schema.Struct({
description: Schema.optional(Schema.String),
location: Schema.String,
content: Schema.String,
+ trusted: Schema.optional(Schema.Boolean), // kilocode_change - gate skill shell injection to trusted sources
})
export type Info = Schema.Schema.Type
@@ -151,6 +152,7 @@ const add = Effect.fnUntraced(function* (state: State, match: Match, events: Eve
description: md.data.description,
location: match.path, // kilocode_change
content: md.content,
+ trusted: match.trusted, // kilocode_change
}
})
@@ -295,6 +297,7 @@ const loadSkills = Effect.fnUntraced(function* (
description: skill.description,
location: BUILTIN_LOCATION,
content: skill.content,
+ trusted: true, // kilocode_change - builtin skills ship in the binary
}
}
// kilocode_change end
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index 4b849895048..ad2049afc2a 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -5,6 +5,11 @@ import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Skill } from "../skill"
import * as Tool from "./tool"
import DESCRIPTION from "./skill.txt"
+// kilocode_change start - gate + run shell injection in skill bodies
+import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
+import { RuntimeFlags } from "@/effect/runtime-flags"
+import { SkillInject } from "@/kilocode/skills/inject"
+// kilocode_change end
export const Parameters = Schema.Struct({
name: Schema.String.annotate({ description: "The name of the skill from available_skills" }),
@@ -15,6 +20,8 @@ export const SkillTool = Tool.define(
Effect.gen(function* () {
const skill = yield* Skill.Service
const ripgrep = yield* Ripgrep.Service
+ const flags = yield* RuntimeFlags.Service // kilocode_change
+ const spawner = yield* ChildProcessSpawner // kilocode_change
return {
description: DESCRIPTION,
@@ -32,6 +39,16 @@ export const SkillTool = Tool.define(
metadata: {},
})
+ // kilocode_change start - render `!`cmd`` shell injection, gated by trust + kill-switch + batch approval
+ const content = yield* SkillInject.render({
+ content: info.content,
+ trusted: info.trusted === true,
+ disabled: flags.disableSkillShell,
+ ctx,
+ spawner,
+ })
+ // kilocode_change end
+
// kilocode_change start - built-in skills have no filesystem directory
if (info.location === Skill.BUILTIN_LOCATION) {
return {
@@ -40,7 +57,7 @@ export const SkillTool = Tool.define(
``,
`# Skill: ${info.name}`,
"",
- info.content.trim(),
+ content.trim(), // kilocode_change
" ",
].join("\n"),
metadata: {
@@ -68,7 +85,7 @@ export const SkillTool = Tool.define(
``,
`# Skill: ${info.name}`,
"",
- info.content.trim(),
+ content.trim(), // kilocode_change
"",
`Base directory for this skill: ${base}`,
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
new file mode 100644
index 00000000000..d6b1fa79811
--- /dev/null
+++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
@@ -0,0 +1,127 @@
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
+import { expect } from "bun:test"
+import { Cause, Effect, Exit, Fiber, Layer } from "effect"
+import { EventV2Bridge } from "@/event-v2-bridge"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Database } from "@opencode-ai/core/database/database"
+import { Permission } from "@/permission"
+import { InstanceBootstrap } from "@/project/bootstrap-service"
+import { InstanceStore } from "@/project/instance-store"
+import { testEffect } from "../../lib/effect"
+import { SessionID } from "@/session/schema"
+import { RuntimeFlags } from "@/effect/runtime-flags"
+import { Config } from "@/config/config"
+
+// skillShell forces a single up-front prompt over soft allow/deny/auto-approve
+// rules, but must never override a hard (plan-mode) veto, and must never be
+// auto-resolved while pending.
+
+const events = EventV2Bridge.defaultLayer
+const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
+const env = Layer.mergeAll(
+ Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)),
+ events,
+ CrossSpawnSpawner.defaultLayer,
+ InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)),
+).pipe(Layer.provide(RuntimeFlags.layer()), Layer.provide(Config.defaultLayer))
+const it = testEffect(Layer.mergeAll(env, RuntimeFlags.layer()))
+
+const ask = (input: Parameters[0]) =>
+ Effect.gen(function* () {
+ return yield* (yield* Permission.Service).ask(input)
+ })
+
+const list = () =>
+ Effect.gen(function* () {
+ return yield* (yield* Permission.Service).list()
+ })
+
+const rejectAll = () =>
+ Effect.gen(function* () {
+ const permission = yield* Permission.Service
+ for (const req of yield* permission.list()) yield* permission.reply({ requestID: req.id, reply: "reject" })
+ })
+
+const waitForPending = (count: number) =>
+ Effect.gen(function* () {
+ const permission = yield* Permission.Service
+ return yield* Effect.gen(function* () {
+ while (true) {
+ const pending = yield* permission.list()
+ if (pending.length === count) return pending
+ yield* Effect.sleep("10 millis")
+ }
+ }).pipe(Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.fail(new Error("timed out")) }))
+ })
+
+const fail = (self: Effect.Effect ) =>
+ Effect.gen(function* () {
+ const exit = yield* self.pipe(Effect.exit)
+ if (Exit.isFailure(exit)) return Cause.squash(exit.cause)
+ throw new Error("expected permission effect to fail")
+ })
+
+it.instance(
+ "skillShell - forces a prompt even when a matching allow rule exists",
+ () =>
+ Effect.gen(function* () {
+ const fiber = yield* ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["printf hi"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
+ }).pipe(Effect.forkScoped)
+
+ expect(yield* waitForPending(1)).toHaveLength(1)
+ yield* rejectAll()
+ yield* Fiber.await(fiber)
+ }),
+ { git: true },
+)
+
+it.instance(
+ "skillShell - is denied by a hard-ruleset veto instead of prompting",
+ () =>
+ Effect.gen(function* () {
+ const err = yield* fail(
+ ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["rm -rf /"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
+ hardRuleset: [{ permission: "bash", pattern: "*", action: "deny" }],
+ }),
+ )
+
+ expect(err).toBeInstanceOf(PermissionV1.DeniedError)
+ expect(yield* list()).toHaveLength(0)
+ }),
+ { git: true },
+)
+
+it.instance(
+ "skillShell - a pending batch is not auto-resolved by allowEverything",
+ () =>
+ Effect.gen(function* () {
+ const fiber = yield* ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["printf hi"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [],
+ }).pipe(Effect.forkScoped)
+
+ expect(yield* waitForPending(1)).toHaveLength(1)
+ yield* (yield* Permission.Service).allowEverything({ enable: true })
+ // still pending: YOLO cannot silently approve a skill batch
+ expect(yield* list()).toHaveLength(1)
+ yield* rejectAll()
+ yield* Fiber.await(fiber)
+ }),
+ { git: true },
+)
diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts
new file mode 100644
index 00000000000..1058968d1e3
--- /dev/null
+++ b/packages/opencode/test/kilocode/skills/inject.test.ts
@@ -0,0 +1,197 @@
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Ripgrep } from "@opencode-ai/core/ripgrep"
+import { Effect, Exit, Layer } from "effect"
+import { afterEach, describe, expect } from "bun:test"
+import fs from "fs"
+import os from "os"
+import path from "path"
+import type { Tool } from "@/tool/tool"
+import { SkillTool } from "@/tool/skill"
+import { SkillInject } from "@/kilocode/skills/inject"
+import { ToolRegistry } from "@/tool/registry"
+import { disposeAllInstances, TestInstance } from "../../fixture/fixture"
+import { SessionID, MessageID } from "@/session/schema"
+import { testEffect } from "../../lib/effect"
+
+// Global (~/.claude) skills are trusted, but Global.Service snapshots the home
+// path when its layer is built, so KILO_TEST_HOME must be set before the runtime
+// layer below is constructed — not inside a test body.
+const HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "skill-inject-home-")))
+process.env.KILO_TEST_HOME = HOME
+
+const baseCtx: Omit = {
+ sessionID: SessionID.make("ses_test"),
+ messageID: MessageID.make("msg_test"),
+ callID: "",
+ agent: "build",
+ abort: AbortSignal.any([]),
+ messages: [],
+ metadata: () => Effect.void,
+}
+
+afterEach(async () => {
+ await disposeAllInstances()
+})
+
+const it = testEffect(
+ Layer.mergeAll(ToolRegistry.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)),
+)
+
+// Shell injection spawns real processes; skip on windows CI like the sibling suite.
+const unix = process.platform !== "win32" ? it.instance : it.instance.skip
+
+afterEach(async () => {
+ // reset discovered skills between tests by clearing the global skill dir
+ await fs.promises.rm(path.join(HOME, ".agents"), { recursive: true, force: true })
+})
+
+// Global ~/.agents skills are trusted (and, unlike ~/.claude, not gated by the
+// KILO_DISABLE_CLAUDE_CODE flag the test env sets); project .kilo skills are untrusted.
+function writeGlobalSkill(name: string, body: string) {
+ return Effect.promise(() =>
+ Bun.write(
+ path.join(HOME, ".agents", "skills", name, "SKILL.md"),
+ `---\nname: ${name}\ndescription: ${name} test skill.\n---\n\n${body}\n`,
+ ),
+ )
+}
+
+function writeProjectSkill(dir: string, name: string, body: string) {
+ return Effect.promise(() =>
+ Bun.write(
+ path.join(dir, ".kilo", "skill", name, "SKILL.md"),
+ `---\nname: ${name}\ndescription: ${name} test skill.\n---\n\n${body}\n`,
+ ),
+ )
+}
+
+function loadSkill(name: string, ask: Tool.Context["ask"]) {
+ return Effect.gen(function* () {
+ const registry = yield* ToolRegistry.Service
+ const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
+ const tool = (yield* registry.tools({
+ providerID: "opencode" as any,
+ modelID: "gpt-5" as any,
+ agent,
+ })).find((t) => t.id === SkillTool.id)
+ if (!tool) throw new Error("Skill tool not found")
+ return yield* tool.execute({ name }, { ...baseCtx, ask })
+ })
+}
+
+describe("skill shell injection", () => {
+ unix("runs the batch after a single forced approval listing every command", () =>
+ Effect.gen(function* () {
+ yield* writeGlobalSkill("trusted-shell", "A: !`printf one` B: !`printf two`")
+
+ const requests: Array> = []
+ const result = yield* loadSkill("trusted-shell", (req) =>
+ Effect.sync(() => {
+ requests.push(req)
+ }),
+ )
+
+ expect(result.output).toContain("A: one B: two")
+ // one skill-load ask plus exactly one batch bash ask carrying all commands
+ const bash = requests.filter((r) => r.permission === "bash")
+ expect(bash.length).toBe(1)
+ expect(bash[0].metadata?.["skillShell"]).toBe(true)
+ // patterns carry the command list the prompt renders
+ expect(bash[0].patterns).toEqual(["printf one", "printf two"])
+ }),
+ )
+
+ unix("aborts the entire skill load when the batch is rejected", () =>
+ Effect.gen(function* () {
+ yield* writeGlobalSkill("denied-shell", "Secret: !`printf leaked`")
+
+ const exit = yield* loadSkill("denied-shell", (req) =>
+ // Reject the batch. Tools wrap ctx.ask with Effect.orDie, so this reaches
+ // the injector as a defect and must abort the whole skill load.
+ req.permission === "bash"
+ ? Effect.fail(new PermissionV1.RejectedError()).pipe(Effect.orDie)
+ : Effect.void,
+ ).pipe(Effect.exit)
+
+ expect(Exit.isFailure(exit)).toBe(true)
+ }),
+ )
+
+ unix("does not run shell injection for untrusted project skills", () =>
+ Effect.gen(function* () {
+ const dir = (yield* TestInstance).directory
+ yield* writeProjectSkill(dir, "untrusted-shell", "Value: !`printf shouldnotrun`")
+
+ const requests: Array> = []
+ const result = yield* loadSkill("untrusted-shell", (req) =>
+ Effect.sync(() => {
+ requests.push(req)
+ }),
+ )
+
+ expect(result.output).toContain("[skill shell execution disabled for untrusted skill]")
+ expect(result.output).not.toContain("shouldnotrun")
+ // no bash permission ask because nothing was scanned or spawned
+ expect(requests.some((r) => r.permission === "bash")).toBe(false)
+ }),
+ )
+
+ unix("does not re-execute shell placeholders emitted by command output", () =>
+ Effect.gen(function* () {
+ // The command emits a literal placeholder `!echo pwned`
+ // built from octal escapes so the SKILL.md itself contains no nested
+ // backticks. If render re-scanned command output, `echo pwned` would run.
+ yield* writeGlobalSkill("nested-shell", "Out: !`printf '!\\140echo pwned\\140'`")
+
+ const result = yield* loadSkill("nested-shell", () => Effect.void)
+
+ expect(result.output).toContain("!`echo pwned`")
+ // "pwned" must appear only inside the inert placeholder, never executed alone.
+ expect(result.output).not.toMatch(/Out:\s*pwned\s*$/m)
+ }),
+ )
+})
+
+// The disabled (kill-switch) and untrusted branches must short-circuit before
+// asking permission or spawning. Passing an ask/spawner that throws proves
+// neither is reached.
+describe("SkillInject.render gating", () => {
+ const boom = () => {
+ throw new Error("must not be reached")
+ }
+ const spawner = new Proxy({}, { get: boom }) as any
+ const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context
+
+ const run = (opts: { trusted: boolean; disabled: boolean; content?: string }) =>
+ Effect.runPromise(
+ SkillInject.render({
+ content: opts.content ?? "Value: !`printf ran`",
+ trusted: opts.trusted,
+ disabled: opts.disabled,
+ ctx,
+ spawner,
+ }),
+ )
+
+ it.effect("kill-switch replaces every command without asking or running it", () =>
+ Effect.gen(function* () {
+ const out = yield* Effect.promise(() => run({ trusted: true, disabled: true }))
+ expect(out).toBe("Value: [skill shell execution disabled by policy]")
+ }),
+ )
+
+ it.effect("untrusted skills never ask or run their commands", () =>
+ Effect.gen(function* () {
+ const out = yield* Effect.promise(() => run({ trusted: false, disabled: false }))
+ expect(out).toBe("Value: [skill shell execution disabled for untrusted skill]")
+ }),
+ )
+
+ it.effect("content without placeholders is returned unchanged", () =>
+ Effect.gen(function* () {
+ const out = yield* Effect.promise(() => run({ trusted: true, disabled: false, content: "no commands here" }))
+ expect(out).toBe("no commands here")
+ }),
+ )
+})
diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx
index 6e712ccd34c..a6f21d0c2fc 100644
--- a/packages/tui/src/routes/session/permission.tsx
+++ b/packages/tui/src/routes/session/permission.tsx
@@ -291,6 +291,20 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
}
if (permission === "bash") {
+ // kilocode_change start - skill shell batches list every command
+ if (props.request.metadata?.["skillShell"] === true) {
+ const commands = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
+ return {
+ icon: "#",
+ title: "Run these skill commands?",
+ body: (
+
+ {(cmd) => {"$ " + cmd} }
+
+ ),
+ }
+ }
+ // kilocode_change end
// kilocode_change start
const meta = props.request.metadata ?? {}
const desc =
@@ -446,10 +460,12 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
)
- // kilocode_change start - hide "Always allow" for protected Kilo configuration access
- const options: Record = props.request.metadata?.[ConfigProtection.DISABLE_ALWAYS_KEY]
- ? { once: "Allow once", reject: "Reject" }
- : { once: "Allow once", always: "Allow always", reject: "Reject" }
+ // kilocode_change start - skill shell batches are never persisted: only Allow / Reject
+ const options: Record = props.request.metadata?.["skillShell"]
+ ? { once: "Allow", reject: "Reject" }
+ : props.request.metadata?.[ConfigProtection.DISABLE_ALWAYS_KEY]
+ ? { once: "Allow once", reject: "Reject" }
+ : { once: "Allow once", always: "Allow always", reject: "Reject" }
// kilocode_change end
const body = (
From 2d784a0e3162818afdb89b2c2352e45fdaaa8c6d Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Tue, 28 Jul 2026 15:27:30 +0200
Subject: [PATCH 13/71] chore(cli): add changeset for skill shell execution
---
.changeset/skill-shell-execution.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/skill-shell-execution.md
diff --git a/.changeset/skill-shell-execution.md b/.changeset/skill-shell-execution.md
new file mode 100644
index 00000000000..bbe09cf70d7
--- /dev/null
+++ b/.changeset/skill-shell-execution.md
@@ -0,0 +1,5 @@
+---
+"@kilocode/cli": minor
+---
+
+Support executing shell commands embedded in skill files. Commands written as `` !`command` `` in a SKILL.md run when the skill loads and their output is inlined into the skill, gated by a single up-front approval that lists every command. Only trusted skills can run commands, and `KILO_DISABLE_SKILL_SHELL` disables the behavior.
From 7650d0fd090d5b8982c962e6483da3035b59488f Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 11:12:34 +0200
Subject: [PATCH 14/71] fix(cli): require human approval for skill shell
command batches
---
packages/core/src/v1/permission.ts | 2 +
.../kilocode/backend/cli/KiloCliDataParser.kt | 1 +
.../views/permission/PermissionView.kt | 2 +-
.../kotlin/ai/kilocode/rpc/dto/ChatDto.kt | 2 +
.../handlers/permission-handler.ts | 2 +-
packages/opencode/src/acp/permission.ts | 5 +-
packages/opencode/src/cli/cmd/run.ts | 3 +-
packages/opencode/src/permission/index.ts | 7 +++
.../instance/httpapi/groups/permission.ts | 1 +
.../instance/httpapi/handlers/permission.ts | 1 +
.../kilocode/permission/skill-shell.test.ts | 50 +++++++++++++++++++
.../tui/src/routes/session/permission.tsx | 2 +
12 files changed, 73 insertions(+), 5 deletions(-)
diff --git a/packages/core/src/v1/permission.ts b/packages/core/src/v1/permission.ts
index b241ccd9077..8c410562e61 100644
--- a/packages/core/src/v1/permission.ts
+++ b/packages/core/src/v1/permission.ts
@@ -45,6 +45,8 @@ export type Reply = typeof Reply.Type
export const ReplyBody = Schema.Struct({
reply: Reply,
message: Schema.String.pipe(Schema.optional),
+ // kilocode_change - set by clients when a human answered the prompt; the server refuses machine approvals of skill-shell batches
+ interactive: Schema.Boolean.pipe(Schema.optional),
}).annotate({ identifier: "PermissionReplyBody" })
export type ReplyBody = typeof ReplyBody.Type
diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt
index 1d4fa2ee4b7..d4e376bb81e 100644
--- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt
+++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt
@@ -1579,6 +1579,7 @@ object KiloCliDataParser {
sb.append("""{"reply":${escape(reply.reply)}""")
val msg = reply.message
if (msg != null) sb.append(""","message":${escape(msg)}""")
+ if (reply.interactive) sb.append(""","interactive":true""")
sb.append("}")
return sb.toString()
}
diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt
index d5323dd58a3..0ae649ef54e 100644
--- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt
+++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt
@@ -356,7 +356,7 @@ class PermissionView(
card.setActionEnabled(ID_RUN, false)
card.setActionEnabled(ID_DENY, false)
rules.setControlsEnabled(false)
- reply(id, PermissionReplyDto(reply = "once"), rulePayload())
+ reply(id, PermissionReplyDto(reply = "once", interactive = true), rulePayload())
}
@RequiresEdt
diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt
index edbbed7b02b..f4e222bbb23 100644
--- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt
+++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt
@@ -328,6 +328,8 @@ data class ToolRefDto(
data class PermissionReplyDto(
val reply: String,
val message: String? = null,
+ // Set when a human answered the prompt; the CLI ignores machine approvals of skill-shell batches.
+ val interactive: Boolean = false,
)
@Serializable
diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts b/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts
index 2b6e3608cac..52008fdab43 100644
--- a/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts
+++ b/packages/kilo-vscode/src/kilo-provider/handlers/permission-handler.ts
@@ -106,7 +106,7 @@ export async function handlePermissionResponse(
}
const replyResult = await ctx.client.permission
- .reply({ requestID: permissionId, reply: response, directory: dir }, { throwOnError: true })
+ .reply({ requestID: permissionId, reply: response, directory: dir, interactive: true }, { throwOnError: true })
.then(() => "ok" as const)
.catch((error: unknown) => {
if (isNotFoundError(error)) return "stale" as const
diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts
index ba07fdf4afc..3a57b6e5419 100644
--- a/packages/opencode/src/acp/permission.ts
+++ b/packages/opencode/src/acp/permission.ts
@@ -81,14 +81,15 @@ export class Handler {
await this.writeProposedEdit(session.id, permission.metadata).catch(() => {})
}
- await this.reply(permission.id, reply, session.cwd)
+ await this.reply(permission.id, reply, session.cwd, true) // kilocode_change - human selected via requestPermission
}
- private async reply(requestID: string, reply: Reply, directory: string) {
+ private async reply(requestID: string, reply: Reply, directory: string, interactive = false) { // kilocode_change - interactive param
await this.input.sdk.permission.reply({
requestID,
reply,
directory,
+ interactive, // kilocode_change
})
}
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index d77916109ed..854ed6d1d9b 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -840,7 +840,8 @@ export const RunCommand = effectCmd({
if (event.type === "permission.asked") {
const permission = event.properties
- // kilocode_change start - skill shell batches need an interactive human decision; never headless auto-approve
+ // kilocode_change start - skill shell batches need an interactive human decision. The server ignores
+ // non-interactive approvals, so headless runs must reject explicitly rather than leave them pending.
if (permission.metadata?.["skillShell"] === true) {
await client.permission.reply({ requestID: permission.id, reply: "reject" })
continue
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index d5217693d20..bed5d68defc 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -303,6 +303,13 @@ export const layer = Layer.effect(
const existing = pending.get(input.requestID)
if (!existing) return yield* new PermissionV1.NotFoundError({ requestID: input.requestID })
+ // kilocode_change start - skill-shell batches must be answered by a human; ignore machine approvals
+ // (auto-approve/YOLO clients omit `interactive`) so the prompt stays pending for a real decision.
+ if (existing.info.metadata?.["skillShell"] === true && input.reply !== "reject" && input.interactive !== true) {
+ return
+ }
+ // kilocode_change end
+
pending.delete(input.requestID)
yield* events.publish(Event.Replied, {
sessionID: existing.info.sessionID,
diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts
index daaa43534fa..b9457ffc033 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts
@@ -12,6 +12,7 @@ const root = "/permission"
const ReplyPayload = Schema.Struct({
reply: PermissionV1.Reply,
message: Schema.optional(Schema.String),
+ interactive: Schema.optional(Schema.Boolean), // kilocode_change - human-answered flag; gates skill-shell approvals
})
// kilocode_change start
diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts
index ce9ee63f4b9..b317cd794cc 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts
@@ -30,6 +30,7 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
requestID: ctx.params.requestID,
reply: ctx.payload.reply,
message: ctx.payload.message,
+ interactive: ctx.payload.interactive, // kilocode_change
})
.pipe(
Effect.catchTag("Permission.NotFoundError", (error) =>
diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
index d6b1fa79811..6815dd3c95e 100644
--- a/packages/opencode/test/kilocode/permission/skill-shell.test.ts
+++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
@@ -42,6 +42,11 @@ const rejectAll = () =>
for (const req of yield* permission.list()) yield* permission.reply({ requestID: req.id, reply: "reject" })
})
+const reply = (input: Parameters[0]) =>
+ Effect.gen(function* () {
+ return yield* (yield* Permission.Service).reply(input)
+ })
+
const waitForPending = (count: number) =>
Effect.gen(function* () {
const permission = yield* Permission.Service
@@ -125,3 +130,48 @@ it.instance(
}),
{ git: true },
)
+
+it.instance(
+ "skillShell - a machine approval (no interactive flag) is ignored and stays pending",
+ () =>
+ Effect.gen(function* () {
+ const fiber = yield* ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["printf hi"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [],
+ }).pipe(Effect.forkScoped)
+
+ const [pending] = yield* waitForPending(1)
+ // An auto-approver replies without `interactive`; the server must ignore it.
+ yield* reply({ requestID: pending.id, reply: "once" })
+ expect(yield* list()).toHaveLength(1)
+ yield* rejectAll()
+ yield* Fiber.await(fiber)
+ }),
+ { git: true },
+)
+
+it.instance(
+ "skillShell - an interactive approval resolves the request",
+ () =>
+ Effect.gen(function* () {
+ const fiber = yield* ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["printf hi"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [],
+ }).pipe(Effect.forkScoped)
+
+ const [pending] = yield* waitForPending(1)
+ yield* reply({ requestID: pending.id, reply: "once", interactive: true })
+ // human approval clears the prompt and the ask succeeds
+ expect(yield* list()).toHaveLength(0)
+ yield* Fiber.await(fiber)
+ }),
+ { git: true },
+)
diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx
index a6f21d0c2fc..77524ba6abc 100644
--- a/packages/tui/src/routes/session/permission.tsx
+++ b/packages/tui/src/routes/session/permission.tsx
@@ -192,6 +192,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
requestID: props.request.id,
directory: props.directory,
workspace: project.workspace.current(),
+ interactive: true, // kilocode_change - human answered this prompt
})
}}
/>
@@ -499,6 +500,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
requestID: props.request.id,
directory: props.directory,
workspace: project.workspace.current(),
+ interactive: true, // kilocode_change - human answered this prompt
})
}}
/>
From 0c417acfaa0c6f6127af5475c0d4fa7ae8d4a984 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 11:14:05 +0200
Subject: [PATCH 15/71] chore(sdk): regenerate for permission reply interactive
flag
---
packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 +
packages/sdk/js/src/v2/gen/types.gen.ts | 2 +
packages/sdk/openapi.json | 296 ++++++++++++++----------
3 files changed, 174 insertions(+), 126 deletions(-)
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 81f6ac2e965..fcdffdc9550 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -3752,6 +3752,7 @@ export class Permission extends HeyApiClient {
workspace?: string
reply?: "once" | "always" | "reject"
message?: string
+ interactive?: boolean
},
options?: Options,
) {
@@ -3765,6 +3766,7 @@ export class Permission extends HeyApiClient {
{ in: "query", key: "workspace" },
{ in: "body", key: "reply" },
{ in: "body", key: "message" },
+ { in: "body", key: "interactive" },
],
},
],
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 61953cc620f..aad0b6b5cbd 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -7717,6 +7717,7 @@ export type AppSkillsResponses = {
description?: string
location: string
content: string
+ trusted?: boolean
}>
}
@@ -8628,6 +8629,7 @@ export type PermissionReplyData = {
body?: {
reply: "once" | "always" | "reject"
message?: string
+ interactive?: boolean
}
path: {
requestID: string
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 53526b5f007..c7cd8731026 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -3137,6 +3137,9 @@
},
"content": {
"type": "string"
+ },
+ "trusted": {
+ "type": "boolean"
}
},
"required": ["name", "location", "content"],
@@ -5312,6 +5315,9 @@
},
"message": {
"type": "string"
+ },
+ "interactive": {
+ "type": "boolean"
}
},
"required": ["reply"],
@@ -24496,6 +24502,15 @@
{
"$ref": "#/components/schemas/EventServerInstanceDisposed"
},
+ {
+ "$ref": "#/components/schemas/EventSessionTurnOpen"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionTurnClose"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionQueueChanged"
+ },
{
"$ref": "#/components/schemas/EventSessionNetworkAsked"
},
@@ -24523,12 +24538,6 @@
{
"$ref": "#/components/schemas/EventInteractive_terminalDeleted"
},
- {
- "$ref": "#/components/schemas/EventSessionTurnOpen"
- },
- {
- "$ref": "#/components/schemas/EventSessionTurnClose"
- },
{
"$ref": "#/components/schemas/EventSandboxStatusChanged"
},
@@ -24557,10 +24566,10 @@
"$ref": "#/components/schemas/EventKilocodeNotebookCancelled"
},
{
- "$ref": "#/components/schemas/EventLspClientDiagnostics"
+ "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
},
{
- "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
+ "$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventMemoryStatus1"
@@ -24818,10 +24827,10 @@
"$ref": "#/components/schemas/EventProjectUpdated"
},
{
- "$ref": "#/components/schemas/EventLspUpdated"
+ "$ref": "#/components/schemas/EventVcsBranchUpdated"
},
{
- "$ref": "#/components/schemas/EventVcsBranchUpdated"
+ "$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventWorkspaceReady"
@@ -26838,7 +26847,7 @@
"type": "object",
"properties": {
"start": {
- "type": "number",
+ "type": "integer",
"minimum": 0
}
},
@@ -26914,11 +26923,11 @@
"type": "object",
"properties": {
"start": {
- "type": "number",
+ "type": "integer",
"minimum": 0
},
"end": {
- "type": "number",
+ "type": "integer",
"minimum": 0
},
"elapsed": {
@@ -27686,6 +27695,15 @@
{
"$ref": "#/components/schemas/EventServerInstanceDisposed"
},
+ {
+ "$ref": "#/components/schemas/EventSessionTurnOpen"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionTurnClose"
+ },
+ {
+ "$ref": "#/components/schemas/EventSessionQueueChanged"
+ },
{
"$ref": "#/components/schemas/EventSessionNetworkAsked"
},
@@ -27713,12 +27731,6 @@
{
"$ref": "#/components/schemas/EventInteractive_terminalDeleted"
},
- {
- "$ref": "#/components/schemas/EventSessionTurnOpen"
- },
- {
- "$ref": "#/components/schemas/EventSessionTurnClose"
- },
{
"$ref": "#/components/schemas/EventSandboxStatusChanged"
},
@@ -27747,10 +27759,10 @@
"$ref": "#/components/schemas/EventKilocodeNotebookCancelled"
},
{
- "$ref": "#/components/schemas/EventLspClientDiagnostics"
+ "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
},
{
- "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
+ "$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventMemoryStatus"
@@ -28008,10 +28020,10 @@
"$ref": "#/components/schemas/EventProjectUpdated"
},
{
- "$ref": "#/components/schemas/EventLspUpdated"
+ "$ref": "#/components/schemas/EventVcsBranchUpdated"
},
{
- "$ref": "#/components/schemas/EventVcsBranchUpdated"
+ "$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventWorkspaceReady"
@@ -35218,6 +35230,96 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventSessionTurnOpen": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.turn.open"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["sessionID"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventSessionTurnClose": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.turn.close"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "parentID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "reason": {
+ "type": "string",
+ "enum": ["completed", "error", "interrupted"]
+ }
+ },
+ "required": ["sessionID", "reason"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
+ "EventSessionQueueChanged": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["session.queue.changed"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses"
+ },
+ "queued": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^msg"
+ }
+ }
+ },
+ "required": ["sessionID", "queued"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"EventSessionNetworkAsked": {
"type": "object",
"properties": {
@@ -35470,64 +35572,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
- "EventSessionTurnOpen": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["session.turn.open"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- }
- },
- "required": ["sessionID"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
- "EventSessionTurnClose": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["session.turn.close"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "sessionID": {
- "type": "string",
- "pattern": "^ses"
- },
- "parentID": {
- "type": "string",
- "pattern": "^ses"
- },
- "reason": {
- "type": "string",
- "enum": ["completed", "error", "interrupted"]
- }
- },
- "required": ["sessionID", "reason"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
"EventSandboxStatusChanged": {
"type": "object",
"properties": {
@@ -35837,33 +35881,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
- "EventLspClientDiagnostics": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["lsp.client.diagnostics"]
- },
- "properties": {
- "type": "object",
- "properties": {
- "serverID": {
- "type": "string"
- },
- "path": {
- "type": "string"
- }
- },
- "required": ["serverID", "path"],
- "additionalProperties": false
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
"EventKilo-sessionsRemote-status-changed": {
"type": "object",
"properties": {
@@ -35891,6 +35908,33 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventLspClientDiagnostics": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["lsp.client.diagnostics"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "serverID": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": ["serverID", "path"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"EventMemoryStatus": {
"type": "object",
"properties": {
@@ -39854,24 +39898,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
- "EventLspUpdated": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "enum": ["lsp.updated"]
- },
- "properties": {
- "type": "object",
- "properties": {}
- }
- },
- "required": ["id", "type", "properties"],
- "additionalProperties": false
- },
"EventVcsBranchUpdated": {
"type": "object",
"properties": {
@@ -39895,6 +39921,24 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
+ "EventLspUpdated": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["lsp.updated"]
+ },
+ "properties": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["id", "type", "properties"],
+ "additionalProperties": false
+ },
"EventWorkspaceReady": {
"type": "object",
"properties": {
From e7a7478b38d81fb785713fa062fcfd601cb05f34 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 11:24:19 +0200
Subject: [PATCH 16/71] fix(cli): keep deny rules terminal for skill shell
command batches
---
packages/opencode/src/permission/index.ts | 19 ++++++----------
.../kilocode/permission/skill-shell.test.ts | 22 +++++++++++++++++++
2 files changed, 29 insertions(+), 12 deletions(-)
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index bed5d68defc..ff6ad14fb07 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -222,19 +222,8 @@ export const layer = Layer.effect(
: false
// kilocode_change end
- // kilocode_change start - skill shell injection always prompts once, overriding every allow/deny/auto-approve rule
- const forceAsk = request.metadata?.["skillShell"] === true
- // kilocode_change end
+ const forceAsk = request.metadata?.["skillShell"] === true // kilocode_change
for (const pattern of request.patterns) {
- // kilocode_change start - force a prompt over soft allow/deny rules, but never over a hard (plan-mode) veto
- if (forceAsk) {
- if (veto(request.permission, pattern, hardRuleset)) {
- return yield* new DeniedError({ ruleset: subset(request.permission, hardRuleset ?? []) })
- }
- needsAsk = true
- continue
- }
- // kilocode_change end
const rule = resolve(request.permission, pattern, ruleset, approved, local) // kilocode_change — include session-scoped rules
yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: rule })
// kilocode_change start — saved/session approvals cannot override hard Ask/Plan denials
@@ -247,6 +236,12 @@ export const layer = Layer.effect(
ruleset: subset(request.permission, ruleset), // kilocode_change
})
}
+ // kilocode_change start - skill shell forces a prompt instead of honoring an allow/auto-approve rule
+ if (forceAsk) {
+ needsAsk = true
+ continue
+ }
+ // kilocode_change end
// kilocode_change start - override "allow" to "ask" for protected config paths
if (rule.action === "allow" && (!isProtected || trusted)) {
approvedRule = rule // remember the winning rule so callers can explain the auto-approval
diff --git a/packages/opencode/test/kilocode/permission/skill-shell.test.ts b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
index 6815dd3c95e..06bf04df7c0 100644
--- a/packages/opencode/test/kilocode/permission/skill-shell.test.ts
+++ b/packages/opencode/test/kilocode/permission/skill-shell.test.ts
@@ -86,6 +86,28 @@ it.instance(
{ git: true },
)
+it.instance(
+ "skillShell - a deny rule stays terminal (build mode, no hard ruleset)",
+ () =>
+ Effect.gen(function* () {
+ // build mode has no hardRuleset; an ordinary deny rule must still block, not prompt.
+ const err = yield* fail(
+ ask({
+ sessionID: SessionID.make("session_test"),
+ permission: "bash",
+ patterns: ["curl evil.sh"],
+ metadata: { skillShell: true },
+ always: [],
+ ruleset: [{ permission: "bash", pattern: "curl *", action: "deny" }],
+ }),
+ )
+
+ expect(err).toBeInstanceOf(PermissionV1.DeniedError)
+ expect(yield* list()).toHaveLength(0)
+ }),
+ { git: true },
+)
+
it.instance(
"skillShell - is denied by a hard-ruleset veto instead of prompting",
() =>
From c2f2831bd996c363279fdf8526a0102c2ff166cf Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 11:38:20 +0200
Subject: [PATCH 17/71] fix(cli): decompose skill shell commands for
per-command permission checks
---
.../opencode/src/kilocode/skills/inject.ts | 47 +++++++++++++++----
packages/opencode/src/tool/shell.ts | 19 +++++++-
packages/opencode/src/tool/skill.ts | 5 ++
.../test/kilocode/skills/inject.test.ts | 26 ++++++++++
4 files changed, 87 insertions(+), 10 deletions(-)
diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts
index ceee78c7427..4bdf6afe58f 100644
--- a/packages/opencode/src/kilocode/skills/inject.ts
+++ b/packages/opencode/src/kilocode/skills/inject.ts
@@ -16,10 +16,12 @@ import type * as Tool from "@/tool/tool"
// skills never spawn a process.
// 2. Kill-switch: `disabled` (KILO_DISABLE_SKILL_SHELL) turns injection off
// entirely, matching Claude's disableSkillShellExecution.
-// 3. Batch approval: every command in the file is presented once, up front, in
-// a single permission prompt (the `skillShell` metadata marker forces this
-// prompt regardless of any allow/deny/auto-approve rule). Approve runs the
-// whole batch; reject aborts the skill load with nothing run.
+// 3. Batch approval: every command in the file is decomposed with the same
+// tree-sitter scan the bash tool uses (per sub-command patterns plus any
+// out-of-project directories), then presented once, up front, in a single
+// permission prompt. The `skillShell` marker forces this prompt regardless
+// of any allow/auto-approve rule; a deny rule or plan-mode veto on any
+// sub-command still blocks. Approve runs the batch; reject aborts the load.
//
// Substitution runs exactly once. Command output is inlined as plain text and is
// never re-scanned, so a command cannot emit a `!`cmd`` placeholder that a later
@@ -29,12 +31,20 @@ const DISABLED_NOTE = "[skill shell execution disabled by policy]"
const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]"
export namespace SkillInject {
+ export type Decompose = (input: {
+ command: string
+ cwd: string
+ shell: string
+ }) => Effect.Effect<{ patterns: string[]; dirs: string[] }>
+
export type Options = {
content: string
trusted: boolean
disabled: boolean
+ cwd: string
ctx: Tool.Context
spawner: Spawner["Service"]
+ decompose: Decompose
}
export const render = Effect.fn("SkillInject.render")(function* (opts: Options) {
@@ -45,20 +55,39 @@ export namespace SkillInject {
if (opts.disabled) return replace(opts.content, () => DISABLED_NOTE)
if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE)
+ const shell = Shell.preferred()
// Deduplicate identical commands so the batch lists and runs each once.
const commands = Array.from(new Set(matches.map(([, cmd]) => cmd)))
- // Single up-front approval for the whole batch. `skillShell` forces one
- // prompt even when rules would allow or deny; a reject/deny propagates as a
- // defect and aborts the skill load without running anything.
+ // Decompose each command into sub-command patterns + out-of-project dir globs
+ // via the shared bash scan, so plan-mode denies and external_directory checks
+ // apply per sub-command instead of matching the raw string as one glob.
+ const patterns = new Set()
+ const dirs = new Set()
+ for (const command of commands) {
+ const scan = yield* opts.decompose({ command, cwd: opts.cwd, shell })
+ for (const pattern of scan.patterns) patterns.add(pattern)
+ for (const dir of scan.dirs) dirs.add(dir)
+ }
+
+ // Single up-front approval. Out-of-project directories are asked first, then
+ // the decomposed sub-commands. `skillShell` forces the prompt over allow/YOLO
+ // rules; a deny/veto on any sub-command propagates as a defect and aborts.
+ if (dirs.size > 0) {
+ yield* opts.ctx.ask({
+ permission: "external_directory",
+ patterns: Array.from(dirs),
+ always: [],
+ metadata: { skillShell: true },
+ })
+ }
yield* opts.ctx.ask({
permission: "bash",
- patterns: commands,
+ patterns: Array.from(patterns),
always: [],
metadata: { skillShell: true },
})
- const shell = Shell.preferred()
const outputs = new Map()
for (const command of commands) {
outputs.set(
diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts
index 27b73acae08..b011f4b3c41 100644
--- a/packages/opencode/src/tool/shell.ts
+++ b/packages/opencode/src/tool/shell.ts
@@ -433,7 +433,24 @@ export const ShellPermission = Effect.gen(function* () {
)
})
- return { ask: check, resolve }
+ // kilocode_change start - expose the tree-sitter scan (sub-command patterns + external-dir globs) for skill-shell batching
+ const dirGlob = (dir: string) =>
+ process.platform === "win32" ? FSUtil.normalizePathPattern(path.join(dir, "*")) : path.join(dir, "*")
+ const decompose = Effect.fn("ShellTool.decompose")(function* (input: { command: string; cwd: string; shell: string }) {
+ const instance = yield* InstanceState.context
+ const ps = Shell.ps(input.shell)
+ return yield* Effect.scoped(
+ Effect.gen(function* () {
+ const tree = yield* Effect.acquireRelease(parse(input.command, ps), (tree) => Effect.sync(() => tree.delete()))
+ const scan = yield* collect(tree.rootNode, input.cwd, ps, input.shell, instance)
+ if (!containsPath(input.cwd, instance)) scan.dirs.add(input.cwd)
+ return { patterns: Array.from(scan.patterns), dirs: Array.from(scan.dirs, dirGlob) }
+ }),
+ )
+ })
+ // kilocode_change end
+
+ return { ask: check, resolve, decompose } // kilocode_change - decompose for skill-shell
})
// kilocode_change end
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index ad2049afc2a..ef2b9b8f549 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -7,7 +7,9 @@ import * as Tool from "./tool"
import DESCRIPTION from "./skill.txt"
// kilocode_change start - gate + run shell injection in skill bodies
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
+import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
+import { ShellPermission } from "./shell"
import { SkillInject } from "@/kilocode/skills/inject"
// kilocode_change end
@@ -22,6 +24,7 @@ export const SkillTool = Tool.define(
const ripgrep = yield* Ripgrep.Service
const flags = yield* RuntimeFlags.Service // kilocode_change
const spawner = yield* ChildProcessSpawner // kilocode_change
+ const permission = yield* ShellPermission // kilocode_change - decompose skill commands like the bash tool
return {
description: DESCRIPTION,
@@ -44,8 +47,10 @@ export const SkillTool = Tool.define(
content: info.content,
trusted: info.trusted === true,
disabled: flags.disableSkillShell,
+ cwd: yield* InstanceState.directory,
ctx,
spawner,
+ decompose: permission.decompose,
})
// kilocode_change end
diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts
index 1058968d1e3..f334bfe5ef0 100644
--- a/packages/opencode/test/kilocode/skills/inject.test.ts
+++ b/packages/opencode/test/kilocode/skills/inject.test.ts
@@ -102,6 +102,29 @@ describe("skill shell injection", () => {
}),
)
+ unix("decomposes a compound command into per-sub-command patterns", () =>
+ Effect.gen(function* () {
+ // A single placeholder with a chained command must not be asked as one glob
+ // pattern (which would let e.g. `cat *` match the whole string). Each
+ // sub-command must appear separately so deny/veto rules apply per command.
+ yield* writeGlobalSkill("compound-shell", "Out: !`cat README.md; printf hi`")
+
+ const requests: Array> = []
+ yield* loadSkill("compound-shell", (req) =>
+ Effect.sync(() => {
+ requests.push(req)
+ }),
+ )
+
+ const bash = requests.filter((r) => r.permission === "bash")
+ expect(bash.length).toBe(1)
+ // both sub-commands are present as distinct patterns, not the raw string
+ expect(bash[0].patterns).toContain("cat README.md")
+ expect(bash[0].patterns).toContain("printf hi")
+ expect(bash[0].patterns).not.toContain("cat README.md; printf hi")
+ }),
+ )
+
unix("aborts the entire skill load when the batch is rejected", () =>
Effect.gen(function* () {
yield* writeGlobalSkill("denied-shell", "Secret: !`printf leaked`")
@@ -162,6 +185,7 @@ describe("SkillInject.render gating", () => {
}
const spawner = new Proxy({}, { get: boom }) as any
const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context
+ const decompose = (() => Effect.sync(boom)) as unknown as SkillInject.Decompose
const run = (opts: { trusted: boolean; disabled: boolean; content?: string }) =>
Effect.runPromise(
@@ -169,8 +193,10 @@ describe("SkillInject.render gating", () => {
content: opts.content ?? "Value: !`printf ran`",
trusted: opts.trusted,
disabled: opts.disabled,
+ cwd: "/tmp",
ctx,
spawner,
+ decompose,
}),
)
From c74d448ffdc0b7f957f16aa8c119e15b54953d32 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 11:57:19 +0200
Subject: [PATCH 18/71] fix(cli): apply skill trust to slash-command shell
execution
---
packages/opencode/src/command/index.ts | 2 ++
packages/opencode/src/session/prompt.ts | 7 +++++-
.../skill-command-autocomplete.test.ts | 24 +++++++++++++++++++
3 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts
index 3160b15f2d9..c4cffa639b8 100644
--- a/packages/opencode/src/command/index.ts
+++ b/packages/opencode/src/command/index.ts
@@ -33,6 +33,7 @@ export const Info = Schema.Struct({
agent: Schema.optional(Schema.String),
model: Schema.optional(Schema.String),
source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])),
+ trusted: Schema.optional(Schema.Boolean), // kilocode_change - skill-sourced templates only run `!`cmd`` shell when trusted
// Some command templates are lazy promises from MCP prompt resolution.
template: Schema.Unknown,
subtask: Schema.optional(Schema.Boolean),
@@ -67,6 +68,7 @@ function fromSkill(item: Skill.Info): Info {
name: item.name,
description: item.description,
source: "skill",
+ trusted: item.trusted === true,
get template() {
return item.content
},
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index f1520cc2e61..83e70f7546d 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -2033,7 +2033,12 @@ export const layer = Layer.effect(
}
const shellMatches = ConfigMarkdown.shell(template)
- if (shellMatches.length > 0) {
+ // kilocode_change start - untrusted skill templates must not spawn shell; mirror the skill tool's trust gate
+ const untrustedSkill = cmd.source === "skill" && cmd.trusted !== true
+ if (shellMatches.length > 0 && untrustedSkill) {
+ template = template.replace(bashRegex, () => "[skill shell execution disabled for untrusted skill]")
+ } else if (shellMatches.length > 0) {
+ // kilocode_change end
const cfg = yield* config.get()
const sh = Shell.preferred(cfg.shell)
// kilocode_change start
diff --git a/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts
index f7e81a786f4..50ea098b034 100644
--- a/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts
+++ b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts
@@ -54,4 +54,28 @@ Skill content.
},
),
)
+
+ // The slash-command path runs a template's `!`cmd`` shell without a permission
+ // prompt, so it must only do so for trusted skills. A project-local skill is
+ // untrusted, and Command.Info carries the flag the prompt executor gates on.
+ it.live("marks project skills untrusted so their slash-command shell is disabled", () =>
+ provideTmpdirInstance(
+ (dir) =>
+ Effect.gen(function* () {
+ yield* Effect.promise(() =>
+ Bun.write(
+ path.join(dir, ".kilo", "skill", "proj", "SKILL.md"),
+ `---\nname: proj\ndescription: proj.\n---\n\nRun: !\`printf hi\`\n`,
+ ),
+ )
+
+ const command = yield* Command.Service
+ const proj = yield* command.get("proj")
+
+ expect(proj?.source).toBe("skill")
+ expect(proj?.trusted).toBe(false)
+ }),
+ { git: true },
+ ),
+ )
})
From cd266e8e55aacf7e56f7d77cb59369b1c55e8632 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 12:17:38 +0200
Subject: [PATCH 19/71] fix(cli): show skill shell commands in ACP permission
prompts
---
packages/opencode/src/acp/permission.ts | 18 +++++++--
packages/opencode/test/acp/permission.test.ts | 38 ++++++++++++++++++-
2 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts
index 3a57b6e5419..8bf102f2175 100644
--- a/packages/opencode/src/acp/permission.ts
+++ b/packages/opencode/src/acp/permission.ts
@@ -16,6 +16,17 @@ const permissionOptions: PermissionOption[] = [
{ optionId: "reject", kind: "reject_once", name: "Reject" },
]
+// kilocode_change start - skill shell batches list their commands and are never persisted, so no "Always allow"
+const skillShellOptions: PermissionOption[] = [
+ { optionId: "once", kind: "allow_once", name: "Allow" },
+ { optionId: "reject", kind: "reject_once", name: "Reject" },
+]
+
+function isSkillShell(metadata: PermissionEvent["properties"]["metadata"]) {
+ return (metadata as { skillShell?: unknown })?.skillShell === true
+}
+// kilocode_change end
+
export class Handler {
private readonly queues = new Map>()
@@ -51,18 +62,19 @@ export class Handler {
return
}
+ const skillShell = isSkillShell(permission.metadata) // kilocode_change - skill batches list commands and never persist
const result = await this.input.connection
.requestPermission({
sessionId: permission.sessionID,
toolCall: {
toolCallId: permission.tool?.callID ?? permission.id,
status: "pending",
- title: permission.permission,
- rawInput: permission.metadata,
+ title: skillShell ? "Run skill shell commands" : permission.permission, // kilocode_change
+ rawInput: skillShell ? { ...permission.metadata, commands: permission.patterns } : permission.metadata, // kilocode_change
kind: toToolKind(permission.permission),
locations: toLocations(permission.permission, permission.metadata),
},
- options: permissionOptions,
+ options: skillShell ? skillShellOptions : permissionOptions, // kilocode_change
})
.catch(async () => {
await this.reply(permission.id, "reject", session.cwd)
diff --git a/packages/opencode/test/acp/permission.test.ts b/packages/opencode/test/acp/permission.test.ts
index 080c8eeceb0..76ce5584c28 100644
--- a/packages/opencode/test/acp/permission.test.ts
+++ b/packages/opencode/test/acp/permission.test.ts
@@ -162,7 +162,11 @@ describe("acp permissions", () => {
{ optionId: "reject", kind: "reject_once", name: "Reject" },
],
})
- expect(harness.replies).toEqual([{ requestID: "perm_1", reply: "once", directory: "/workspace" }])
+ // kilocode_change start - human selections are marked interactive
+ expect(harness.replies).toEqual([
+ { requestID: "perm_1", reply: "once", directory: "/workspace", interactive: true },
+ ])
+ // kilocode_change end
})
it("forwards external_directory metadata and locations to requestPermission", async () => {
@@ -201,6 +205,38 @@ describe("acp permissions", () => {
})
})
+ // kilocode_change start - skill shell batches surface their command list and cannot be persisted
+ it("forwards skill shell commands and omits the persist option", async () => {
+ const harness = createHarness()
+ await createSession(harness.session, "ses_a")
+
+ harness.subscription.handle(
+ permissionAsked("ses_a", "perm_skill", {
+ permission: "bash",
+ metadata: { skillShell: true },
+ tool: { messageID: "msg_1", callID: "call_1" },
+ }),
+ )
+
+ await pollUntil(() => harness.replies.length === 1, "skill shell permission was never replied")
+
+ expect(harness.requests[0]).toMatchObject({
+ toolCall: {
+ title: "Run skill shell commands",
+ rawInput: { skillShell: true, commands: ["*"] },
+ },
+ // no allow_always: skill shell is never persisted
+ options: [
+ { optionId: "once", kind: "allow_once", name: "Allow" },
+ { optionId: "reject", kind: "reject_once", name: "Reject" },
+ ],
+ })
+ expect(harness.requests[0].options.some((o) => o.kind === "allow_always")).toBe(false)
+ // the human selection is marked interactive so the server accepts the approval
+ expect(harness.replies[0]).toMatchObject({ requestID: "perm_skill", reply: "once", interactive: true })
+ })
+ // kilocode_change end
+
it("rejects non-selected outcomes", async () => {
const harness = createHarness(() => Promise.resolve({ outcome: { outcome: "cancelled" } }))
await createSession(harness.session, "ses_a")
From 3cef158ef058095d30f851c83747ff3c7ed4c201 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 12:27:21 +0200
Subject: [PATCH 20/71] fix(cli): confine downloaded skills to the cache
directory
---
packages/opencode/src/skill/discovery.ts | 16 ++++++++++++-
.../opencode/test/skill/discovery.test.ts | 23 +++++++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts
index 0495bc637dc..24e78c14f81 100644
--- a/packages/opencode/src/skill/discovery.ts
+++ b/packages/opencode/src/skill/discovery.ts
@@ -71,8 +71,22 @@ export const layer: Layer.Layer skill.files.includes("SKILL.md"))
+ // kilocode_change start - remote index.json controls skill.name/file, so a crafted `../` could escape the
+ // cache and plant a SKILL.md in a trusted dir (e.g. ~/.agents/skills). Drop any skill whose paths escape it.
+ const rooted = (target: string) => {
+ const rel = path.relative(cache, target)
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
+ }
+ const safe: typeof list = []
+ for (const skill of list) {
+ const root = path.join(cache, skill.name)
+ if (rooted(root) && skill.files.every((file) => rooted(path.join(root, file)))) safe.push(skill)
+ else yield* Effect.logWarning("skipping skill with unsafe path", { url: index, skill: skill.name })
+ }
+ // kilocode_change end
+
const dirs = yield* Effect.forEach(
- list,
+ safe, // kilocode_change - was `list`; drop skills whose paths escape the cache
(skill) =>
Effect.gen(function* () {
const root = path.join(cache, skill.name)
diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts
index 5dc5d5195bb..ad1a53463f4 100644
--- a/packages/opencode/test/skill/discovery.test.ts
+++ b/packages/opencode/test/skill/discovery.test.ts
@@ -24,6 +24,15 @@ beforeAll(async () => {
async fetch(req) {
const url = new URL(req.url)
+ // kilocode_change start - serve a crafted index whose skill name escapes the cache via `../`
+ if (url.pathname === "/evil/index.json") {
+ return Response.json({ skills: [{ name: "../../../.agents/skills/evil", files: ["SKILL.md"] }] })
+ }
+ if (url.pathname.endsWith("/.agents/skills/evil/SKILL.md")) {
+ return new Response("---\nname: evil\ndescription: evil.\n---\npwned")
+ }
+ // kilocode_change end
+
// route /.well-known/skills/* to the fixture directory
if (url.pathname.startsWith("/.well-known/skills/")) {
const filePath = url.pathname.replace("/.well-known/skills/", "")
@@ -114,6 +123,20 @@ describe("Discovery.pull", () => {
}),
)
+ // kilocode_change start - path-traversal in the remote index must not plant a trusted skill
+ it.live("rejects a skill name that escapes the cache directory", () =>
+ Effect.gen(function* () {
+ const fsys = yield* FSUtil.Service
+ const discovery = yield* Discovery.Service
+ const dirs = yield* discovery.pull(`http://localhost:${server.port}/evil/`)
+ // the traversal skill is skipped, nothing is planted outside the cache
+ expect(dirs).toEqual([])
+ const escaped = path.join(cacheDir, "../../../.agents/skills/evil/SKILL.md")
+ expect(yield* fsys.existsSafe(escaped)).toBe(false)
+ }),
+ )
+ // kilocode_change end
+
it.live("caches downloaded files on second pull", () =>
Effect.gen(function* () {
// clear dir and downloadCount
From 2d8377894dae4382fe8b624dce2466b8c5d10c77 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 12:36:27 +0200
Subject: [PATCH 21/71] fix(cli): bound skill shell execution (cwd, abort,
timeout, output cap)
---
.../opencode/src/kilocode/skills/inject.ts | 45 ++++++++++++++-----
packages/opencode/src/tool/skill.ts | 3 --
.../test/kilocode/skills/inject.test.ts | 32 ++++++++++++-
3 files changed, 65 insertions(+), 15 deletions(-)
diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts
index 4bdf6afe58f..4c5d8f98460 100644
--- a/packages/opencode/src/kilocode/skills/inject.ts
+++ b/packages/opencode/src/kilocode/skills/inject.ts
@@ -1,8 +1,6 @@
import { Effect } from "effect"
-import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
-import type { ChildProcessSpawner as Spawner } from "effect/unstable/process/ChildProcessSpawner"
import { ConfigMarkdown } from "@/config/markdown"
-import { CommandTimeout } from "@/kilocode/command-timeout"
+import { Process } from "@/util/process"
import { Shell } from "@opencode-ai/core/shell"
import type * as Tool from "@/tool/tool"
@@ -30,6 +28,12 @@ import type * as Tool from "@/tool/tool"
const DISABLED_NOTE = "[skill shell execution disabled by policy]"
const UNTRUSTED_NOTE = "[skill shell execution disabled for untrusted skill]"
+// Execution bounds: model-initiated commands must not hang the load, blow up
+// context, or overrun the batch.
+const TIMEOUT_MS = 2 * 60 * 1000
+const MAX_OUTPUT_BYTES = 32 * 1024
+const MAX_COMMANDS = 32
+
export namespace SkillInject {
export type Decompose = (input: {
command: string
@@ -43,7 +47,6 @@ export namespace SkillInject {
disabled: boolean
cwd: string
ctx: Tool.Context
- spawner: Spawner["Service"]
decompose: Decompose
}
@@ -56,8 +59,9 @@ export namespace SkillInject {
if (!opts.trusted) return replace(opts.content, () => UNTRUSTED_NOTE)
const shell = Shell.preferred()
- // Deduplicate identical commands so the batch lists and runs each once.
- const commands = Array.from(new Set(matches.map(([, cmd]) => cmd)))
+ // Deduplicate identical commands, then cap the batch so a skill can't queue
+ // an unbounded number of processes.
+ const commands = Array.from(new Set(matches.map(([, cmd]) => cmd))).slice(0, MAX_COMMANDS)
// Decompose each command into sub-command patterns + out-of-project dir globs
// via the shared bash scan, so plan-mode denies and external_directory checks
@@ -88,17 +92,38 @@ export namespace SkillInject {
metadata: { skillShell: true },
})
+ // Run each command in the instance directory, bounded by ctx.abort (ESC) and a
+ // timeout, with output truncated so it can't blow up or poison the prompt.
const outputs = new Map()
for (const command of commands) {
- outputs.set(
- command,
- yield* CommandTimeout.text(command, shell).pipe(Effect.provideService(ChildProcessSpawner, opts.spawner)),
- )
+ outputs.set(command, yield* run(command, shell, opts.cwd, opts.ctx.abort))
}
return replace(opts.content, (command) => outputs.get(command) ?? "")
})
+ const run = Effect.fn("SkillInject.run")(function* (command: string, shell: string, cwd: string, abort: AbortSignal) {
+ const result = yield* Effect.promise(async () => {
+ // A cleared timer bounds the run without leaking a pending 2-minute timeout
+ // per command; ESC (ctx.abort) still kills the child via the same signal.
+ const controller = new AbortController()
+ const signal = AbortSignal.any([abort, controller.signal])
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
+ try {
+ return await Process.text([command], { shell, cwd, abort: signal, nothrow: true }).catch(() => undefined)
+ } finally {
+ clearTimeout(timer)
+ }
+ })
+ if (!result) return abort.aborted ? "[skill shell command aborted]" : "[skill shell command timed out]"
+ return truncate(result.text)
+ })
+
+ function truncate(text: string) {
+ if (Buffer.byteLength(text) <= MAX_OUTPUT_BYTES) return text
+ return text.slice(0, MAX_OUTPUT_BYTES) + "\n[skill shell output truncated]"
+ }
+
// Replace only the exact matches found in the ORIGINAL content. Never re-scan
// the result, so inlined output containing `!`cmd`` stays inert.
function replace(content: string, value: (command: string) => string) {
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index ef2b9b8f549..06e1bf93b32 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -6,7 +6,6 @@ import { Skill } from "../skill"
import * as Tool from "./tool"
import DESCRIPTION from "./skill.txt"
// kilocode_change start - gate + run shell injection in skill bodies
-import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ShellPermission } from "./shell"
@@ -23,7 +22,6 @@ export const SkillTool = Tool.define(
const skill = yield* Skill.Service
const ripgrep = yield* Ripgrep.Service
const flags = yield* RuntimeFlags.Service // kilocode_change
- const spawner = yield* ChildProcessSpawner // kilocode_change
const permission = yield* ShellPermission // kilocode_change - decompose skill commands like the bash tool
return {
@@ -49,7 +47,6 @@ export const SkillTool = Tool.define(
disabled: flags.disableSkillShell,
cwd: yield* InstanceState.directory,
ctx,
- spawner,
decompose: permission.decompose,
})
// kilocode_change end
diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts
index f334bfe5ef0..b0b9e00f7ff 100644
--- a/packages/opencode/test/kilocode/skills/inject.test.ts
+++ b/packages/opencode/test/kilocode/skills/inject.test.ts
@@ -102,6 +102,18 @@ describe("skill shell injection", () => {
}),
)
+ unix("runs commands in the instance directory", () =>
+ Effect.gen(function* () {
+ const dir = (yield* TestInstance).directory
+ yield* writeGlobalSkill("cwd-shell", "Here: !`pwd`")
+
+ const result = yield* loadSkill("cwd-shell", () => Effect.void)
+
+ // pwd resolves to the instance dir (realpath), not the server process cwd
+ expect(result.output).toContain(path.basename(dir))
+ }),
+ )
+
unix("decomposes a compound command into per-sub-command patterns", () =>
Effect.gen(function* () {
// A single placeholder with a chained command must not be asked as one glob
@@ -174,6 +186,24 @@ describe("skill shell injection", () => {
expect(result.output).not.toMatch(/Out:\s*pwned\s*$/m)
}),
)
+
+ unix("truncates oversized command output before inlining", () =>
+ Effect.gen(function* () {
+ const dir = (yield* TestInstance).directory
+ // render directly (bypassing the tool's own output truncation) to assert the injector caps output
+ const rendered = yield* SkillInject.render({
+ content: "Out: !`yes x | head -c 65536`",
+ trusted: true,
+ disabled: false,
+ cwd: dir,
+ ctx: { ...baseCtx, ask: () => Effect.void } as Tool.Context,
+ decompose: ({ command }) => Effect.succeed({ patterns: [command], dirs: [] }),
+ })
+
+ expect(rendered).toContain("[skill shell output truncated]")
+ expect(rendered.length).toBeLessThan(40000)
+ }),
+ )
})
// The disabled (kill-switch) and untrusted branches must short-circuit before
@@ -183,7 +213,6 @@ describe("SkillInject.render gating", () => {
const boom = () => {
throw new Error("must not be reached")
}
- const spawner = new Proxy({}, { get: boom }) as any
const ctx = { ...baseCtx, ask: () => Effect.sync(boom) } as Tool.Context
const decompose = (() => Effect.sync(boom)) as unknown as SkillInject.Decompose
@@ -195,7 +224,6 @@ describe("SkillInject.render gating", () => {
disabled: opts.disabled,
cwd: "/tmp",
ctx,
- spawner,
decompose,
}),
)
From 8f4725729e1be1cdb46a5ad2a7690d343e12bdf5 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 12:53:01 +0200
Subject: [PATCH 22/71] fix(cli): harden skill shell permission prompt display
---
packages/opencode/src/kilocode/skills/display.ts | 11 +++++++++++
packages/opencode/src/kilocode/skills/inject.ts | 5 +++--
packages/opencode/src/tool/skill.ts | 1 +
.../opencode/test/kilocode/skills/display.test.ts | 15 +++++++++++++++
.../opencode/test/kilocode/skills/inject.test.ts | 2 ++
packages/tui/src/routes/session/permission.tsx | 9 ++++++---
6 files changed, 38 insertions(+), 5 deletions(-)
create mode 100644 packages/opencode/src/kilocode/skills/display.ts
create mode 100644 packages/opencode/test/kilocode/skills/display.test.ts
diff --git a/packages/opencode/src/kilocode/skills/display.ts b/packages/opencode/src/kilocode/skills/display.ts
new file mode 100644
index 00000000000..93e8ac44f27
--- /dev/null
+++ b/packages/opencode/src/kilocode/skills/display.ts
@@ -0,0 +1,11 @@
+// Render a skill command for a permission prompt as a single, tamper-evident
+// line: escape control chars (CR/LF/ESC/etc.) so a command can't repaint the
+// terminal to make the visible text differ from what will execute.
+export function displayCommand(command: string) {
+ return command.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch) => {
+ if (ch === "\n") return "\\n"
+ if (ch === "\r") return "\\r"
+ if (ch === "\t") return "\\t"
+ return "\\x" + ch.charCodeAt(0).toString(16).padStart(2, "0")
+ })
+}
diff --git a/packages/opencode/src/kilocode/skills/inject.ts b/packages/opencode/src/kilocode/skills/inject.ts
index 4c5d8f98460..3c738ad3d63 100644
--- a/packages/opencode/src/kilocode/skills/inject.ts
+++ b/packages/opencode/src/kilocode/skills/inject.ts
@@ -46,6 +46,7 @@ export namespace SkillInject {
trusted: boolean
disabled: boolean
cwd: string
+ skill: string
ctx: Tool.Context
decompose: Decompose
}
@@ -82,14 +83,14 @@ export namespace SkillInject {
permission: "external_directory",
patterns: Array.from(dirs),
always: [],
- metadata: { skillShell: true },
+ metadata: { skillShell: true, skill: opts.skill },
})
}
yield* opts.ctx.ask({
permission: "bash",
patterns: Array.from(patterns),
always: [],
- metadata: { skillShell: true },
+ metadata: { skillShell: true, skill: opts.skill },
})
// Run each command in the instance directory, bounded by ctx.abort (ESC) and a
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index 06e1bf93b32..758696b02a0 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -46,6 +46,7 @@ export const SkillTool = Tool.define(
trusted: info.trusted === true,
disabled: flags.disableSkillShell,
cwd: yield* InstanceState.directory,
+ skill: info.name,
ctx,
decompose: permission.decompose,
})
diff --git a/packages/opencode/test/kilocode/skills/display.test.ts b/packages/opencode/test/kilocode/skills/display.test.ts
new file mode 100644
index 00000000000..b7780e5138c
--- /dev/null
+++ b/packages/opencode/test/kilocode/skills/display.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from "bun:test"
+import { displayCommand } from "@/kilocode/skills/display"
+
+describe("displayCommand", () => {
+ it("escapes control characters so a command cannot repaint the prompt", () => {
+ // CR/ESC would otherwise let the visible text differ from what executes
+ const out = displayCommand("echo ok\r\x1b[2Krm -rf /\nnext")
+ expect(out).toBe("echo ok\\r\\x1b[2Krm -rf /\\nnext")
+ expect(out).not.toMatch(/[\u0000-\u001f]/)
+ })
+
+ it("leaves ordinary commands unchanged", () => {
+ expect(displayCommand("git status --short")).toBe("git status --short")
+ })
+})
diff --git a/packages/opencode/test/kilocode/skills/inject.test.ts b/packages/opencode/test/kilocode/skills/inject.test.ts
index b0b9e00f7ff..f0e4f7f82e5 100644
--- a/packages/opencode/test/kilocode/skills/inject.test.ts
+++ b/packages/opencode/test/kilocode/skills/inject.test.ts
@@ -196,6 +196,7 @@ describe("skill shell injection", () => {
trusted: true,
disabled: false,
cwd: dir,
+ skill: "big-shell",
ctx: { ...baseCtx, ask: () => Effect.void } as Tool.Context,
decompose: ({ command }) => Effect.succeed({ patterns: [command], dirs: [] }),
})
@@ -223,6 +224,7 @@ describe("SkillInject.render gating", () => {
trusted: opts.trusted,
disabled: opts.disabled,
cwd: "/tmp",
+ skill: "test",
ctx,
decompose,
}),
diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx
index 77524ba6abc..6b1b391fcad 100644
--- a/packages/tui/src/routes/session/permission.tsx
+++ b/packages/tui/src/routes/session/permission.tsx
@@ -19,6 +19,7 @@ import { ConfigProtection } from "@/kilocode/permission/config-paths"
import { splitDiffHunks } from "@/kilocode/tui/diff"
import { normalizeUrls } from "@/kilocode/util/url"
import { MemoryPermissionRegistry } from "@/kilocode/cli/cmd/tui/routes/session/memory-permission"
+import { displayCommand } from "@/kilocode/skills/display"
// kilocode_change end
import { KILO_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
import { usePathFormatter } from "../../context/path-format"
@@ -292,15 +293,17 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
}
if (permission === "bash") {
- // kilocode_change start - skill shell batches list every command
+ // kilocode_change start - skill shell batches list every command, control-char-escaped so the
+ // displayed command cannot repaint the line to differ from what executes
if (props.request.metadata?.["skillShell"] === true) {
const commands = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
+ const skill = typeof props.request.metadata?.["skill"] === "string" ? props.request.metadata["skill"] : undefined
return {
icon: "#",
- title: "Run these skill commands?",
+ title: skill ? `Run shell commands from skill "${skill}"?` : "Run these skill commands?",
body: (
- {(cmd) => {"$ " + cmd} }
+ {(cmd) => {"$ " + displayCommand(cmd)} }
),
}
From 9275fa41932b792a83cf0239a9e401d044a992a3 Mon Sep 17 00:00:00 2001
From: Bruno Agatao
Date: Wed, 29 Jul 2026 13:11:03 +0200
Subject: [PATCH 23/71] test(vscode): expect interactive flag in permission
reply assertions
---
.../tests/unit/permission-recovery.test.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/packages/kilo-vscode/tests/unit/permission-recovery.test.ts b/packages/kilo-vscode/tests/unit/permission-recovery.test.ts
index df7a44d25d6..406076ec606 100644
--- a/packages/kilo-vscode/tests/unit/permission-recovery.test.ts
+++ b/packages/kilo-vscode/tests/unit/permission-recovery.test.ts
@@ -141,7 +141,9 @@ describe("handlePermissionResponse", () => {
await handlePermissionResponse(fake, "p1", "s1", "once", [], [])
- expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }])
+ expect(replies).toEqual([
+ { requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature", interactive: true },
+ ])
})
it("saves selected rules and replies in the recorded SSE directory", async () => {
@@ -158,7 +160,9 @@ describe("handlePermissionResponse", () => {
deniedAlways: ["rm *"],
},
])
- expect(replies).toEqual([{ requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature" }])
+ expect(replies).toEqual([
+ { requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature", interactive: true },
+ ])
})
it("treats an SDK-wrapped 404 while saving rules as stale", async () => {
@@ -192,7 +196,9 @@ describe("handlePermissionResponse", () => {
await handlePermissionResponse(fake, "p1", "s1", "once", [], [])
- expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }])
+ expect(replies).toEqual([
+ { requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature", interactive: true },
+ ])
expect(permDirs.has("p1")).toBe(false)
expect(messages).toEqual([{ type: "permissionError", permissionID: "p1", stale: true }])
})
From 3ab1122e0fbbff5c6821acb21df7d3d93f52955f Mon Sep 17 00:00:00 2001
From: marius-kilocode
Date: Wed, 29 Jul 2026 14:01:00 +0200
Subject: [PATCH 24/71] feat(agent-manager): add diff scope selector and base
branch picker
---
.../agent-manager-diff-scope-selector.md | 5 +
...00000-agent-manager-diff-scope-selector.md | 339 ++++++++++++++++++
.../src/agent-manager/AgentManagerProvider.ts | 44 ++-
.../src/agent-manager/diff-scope.ts | 57 +++
.../kilo-vscode/src/agent-manager/types.ts | 32 ++
.../agent-manager/worktree-diff-controller.ts | 193 +++++-----
.../kilo-vscode/src/diff/sources/catalog.ts | 13 +-
.../kilo-vscode/src/diff/sources/staged.ts | 38 +-
.../kilo-vscode/src/diff/sources/unstaged.ts | 38 +-
.../kilo-vscode/src/diff/sources/worktree.ts | 61 +++-
packages/kilo-vscode/src/diff/types.ts | 21 ++
.../tests/unit/agent-manager-arch.test.ts | 4 +-
.../kilo-vscode/tests/unit/diff-scope.test.ts | 52 +++
.../agent-manager/AgentManagerApp.tsx | 171 ++++-----
.../webview-ui/agent-manager/DiffPanel.tsx | 11 +-
.../agent-manager/agent-manager.css | 9 +
.../webview-ui/agent-manager/diff-messages.ts | 74 ++++
.../agent-manager/diff-review-scope.ts | 120 +++++++
.../agent-manager/diff-scope-state.ts | 91 +++++
.../webview-ui/agent-manager/i18n/ar.ts | 1 +
.../webview-ui/agent-manager/i18n/br.ts | 1 +
.../webview-ui/agent-manager/i18n/bs.ts | 1 +
.../webview-ui/agent-manager/i18n/da.ts | 1 +
.../webview-ui/agent-manager/i18n/de.ts | 1 +
.../webview-ui/agent-manager/i18n/en.ts | 1 +
.../webview-ui/agent-manager/i18n/es.ts | 1 +
.../webview-ui/agent-manager/i18n/fr.ts | 1 +
.../webview-ui/agent-manager/i18n/it.ts | 1 +
.../webview-ui/agent-manager/i18n/ja.ts | 1 +
.../webview-ui/agent-manager/i18n/ko.ts | 1 +
.../webview-ui/agent-manager/i18n/nl.ts | 1 +
.../webview-ui/agent-manager/i18n/no.ts | 1 +
.../webview-ui/agent-manager/i18n/pl.ts | 1 +
.../webview-ui/agent-manager/i18n/ru.ts | 1 +
.../webview-ui/agent-manager/i18n/th.ts | 1 +
.../webview-ui/agent-manager/i18n/tr.ts | 1 +
.../webview-ui/agent-manager/i18n/uk.ts | 1 +
.../webview-ui/agent-manager/i18n/zh.ts | 1 +
.../webview-ui/agent-manager/i18n/zht.ts | 1 +
.../webview-ui/agent-manager/revert-file.ts | 17 +-
.../diff-viewer/DiffScopeControls.tsx | 55 +++
.../diff-viewer/FullScreenDiffView.tsx | 5 +-
.../src/types/messages/extension-messages.ts | 13 +
.../src/types/messages/webview-messages.ts | 21 ++
44 files changed, 1287 insertions(+), 217 deletions(-)
create mode 100644 .changeset/agent-manager-diff-scope-selector.md
create mode 100644 .kilo/plans/1784100000000-agent-manager-diff-scope-selector.md
create mode 100644 packages/kilo-vscode/src/agent-manager/diff-scope.ts
create mode 100644 packages/kilo-vscode/tests/unit/diff-scope.test.ts
create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-messages.ts
create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-review-scope.ts
create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/diff-scope-state.ts
create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/DiffScopeControls.tsx
diff --git a/.changeset/agent-manager-diff-scope-selector.md b/.changeset/agent-manager-diff-scope-selector.md
new file mode 100644
index 00000000000..341e5ee7684
--- /dev/null
+++ b/.changeset/agent-manager-diff-scope-selector.md
@@ -0,0 +1,5 @@
+---
+"kilo-code": minor
+---
+
+Add a scope selector and base branch picker to the Agent Manager diff review. The side panel and full-screen review now let you switch between Branch, Staged, Unstaged, and Session scopes for the selected worktree, and the Branch scope's base branch can be overridden from a picker next to it. Branch stays the default, so existing review behavior is unchanged.
diff --git a/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md b/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md
new file mode 100644
index 00000000000..f827a2c3417
--- /dev/null
+++ b/.kilo/plans/1784100000000-agent-manager-diff-scope-selector.md
@@ -0,0 +1,339 @@
+# Bring the Changes scope selector and base branch picker into Agent Manager
+
+## Goal
+
+The standalone **Changes** editor panel has a scope selector (`GIT: Branch / Staged / Unstaged`,
+`SESSION: Session`) plus a base branch picker (`main → origin/main [Default]`). Agent Manager has
+two diff surfaces (compact side panel and full-screen review tab) with **no scope selector and no
+base picker**: it always shows one fixed scope against one fixed base.
+
+Make both Agent Manager surfaces scope-aware and base-aware, reusing the existing components and
+extension-side sources rather than duplicating them.
+
+## What exists today
+
+### Standalone Changes panel
+
+| Concern | Where |
+|---|---|
+| Panel host, ephemeral base override | `src/diff/DiffViewerProvider.ts:26-33`, `:101-119`, `:170-182` |
+| Source enumeration and construction | `src/diff/sources/catalog.ts:73-115` |
+| Scope select | `webview-ui/diff-viewer/DiffPickerHeader.tsx:50-86` |
+| Base picker | `webview-ui/diff-viewer/BaseBranchPicker.tsx:77-142` |
+| Branch list + auto base + HEAD | `src/diff/sources/catalog.ts:117-138` |
+| Renderer | `webview-ui/diff-viewer/FullScreenDiffView.tsx:95` |
+
+Sources: `worktree.ts` (Branch), `staged.ts`, `unstaged.ts`, `session.ts`, `turn.ts`. Polling,
+dedupe, and lazy per-file detail live in `src/diff/SourceController.ts`.
+
+### Agent Manager
+
+| Concern | Where |
+|---|---|
+| Diff controller (wraps the same `SourceController`) | `src/agent-manager/worktree-diff-controller.ts:35-80` |
+| Synthetic single source, hardcoded capabilities | `src/agent-manager/worktree-diff-controller.ts:228-242` |
+| Fixed base = `origin/` | `src/agent-manager/worktree-diff-controller.ts:217`, `WorktreeStateManager.ts:57-64` |
+| `local` pseudo-context uses auto base | `src/agent-manager/worktree-diff-controller.ts:220-222` |
+| Side panel | `webview-ui/agent-manager/DiffPanel.tsx:474-524` |
+| Full-screen review (shared component) | `webview-ui/agent-manager/AgentManagerApp.tsx:3105-3131` |
+| Data store keyed by session id | `webview-ui/agent-manager/AgentManagerApp.tsx:1675-1699` |
+
+So both systems already share `SourceController`, `local-diff.ts`, `GitOps`, `FullScreenDiffView`,
+`FileTree`, `diff-state.ts`, `diff-requests.ts`. The gap is only the *selection* layer.
+
+## The architectural mismatch to resolve first
+
+The two systems key diff sources along orthogonal axes:
+
+- Standalone: keyed by **scope** (`workspace`, `staged`, `unstaged`, `session:`) inside one
+ fixed directory (`getWorkspaceRoot()`).
+- Agent Manager: keyed by **context** (`sessionId` or `local`), which resolves to a directory and
+ base, with one fixed scope.
+
+Integration therefore needs a composite key `(context, scope)`:
+
+```
+ctx = "local" | ""
+scope = "branch" | "staged" | "unstaged" | "session"
+id = `${ctx}#${scope}`
+```
+
+`ctx#branch` is the default and reproduces today's behavior exactly.
+
+### Hard prerequisite: sources must accept an explicit directory
+
+Three sources resolve the workspace root themselves and cannot currently point at a worktree:
+
+- `src/diff/sources/worktree.ts:46`, `:60`
+- `src/diff/sources/staged.ts:47`
+- `src/diff/sources/unstaged.ts:53`
+- `src/diff/sources/catalog.ts:118` (`listWorkspaceBranches`)
+
+`session.ts` is already directory-parameterized (`catalog.ts:111`), so Session scope is nearly free.
+
+Each source also constructs its own `GitOps` + `OutputChannel` (`worktree.ts:35-37`,
+`staged.ts:43-45`). That is acceptable in the standalone panel where sources swap only on scope
+change, but Agent Manager swaps sources on **every session selection**. Inject a `log` function and
+reuse Agent Manager's shared `GitOps` (`AgentManagerProvider.ts:140-180`) instead of constructing
+per source.
+
+## Is Branch still the right default per worktree?
+
+Yes, keep `Branch` as the default for every worktree context. Reasons, strongest first:
+
+1. **It matches what you ship.** Branch is `merge-base(HEAD, origin/) → current working
+ tree`: committed work, staged, unstaged, and untracked. That is exactly the payload
+ `Apply to local` builds (`GitOps.buildWorktreePatch`, used at
+ `worktree-diff-controller.ts:115`) and what a PR from that branch would contain. Reviewing the
+ same set you apply or push is the whole point of the review tab.
+2. **No silent behavior change.** It is what Agent Manager does today, and the sidebar `Nf +N -N`
+ badge (`GitStatsPoller.ts:191-224`) uses the same base. A different default would make the
+ badge and the review disagree on first open.
+3. **Stable under base movement.** merge-base semantics mean commits landing on `origin/main`
+ after the worktree branched do not pollute the diff.
+4. **Session scope can be legitimately empty.** It depends on snapshots being enabled and degrades
+ to a `snapshots-disabled` notice (`sources/session.ts:33-66`). A default that can be empty for
+ configuration reasons is a bad default.
+
+One correction to the framing: Branch compares against the base **ref** (`origin/main`), not
+against the local main checkout's working tree. If your local `main` has unpushed commits or dirty
+files, the worktree diff does not account for them. "What changes if I apply this to my current
+checkout" is a different question, answered today only by the `Apply to local` conflict check. A
+`Local workspace` scope could answer it directly, but it is a non-goal here (see below).
+
+## What each scope means in Agent Manager
+
+| Scope | Worktree context | Local context | Value |
+|---|---|---|---|
+| Branch | `merge-base(HEAD, origin/)` to working tree | `merge-base(HEAD, auto base)` to working tree | Default. The reviewable/shippable set. |
+| Staged | index vs `HEAD` inside the worktree | same, workspace root | "What did I stage for the next commit." Read-only. |
+| Unstaged | working tree vs index, plus untracked | same | "What is not committed yet." Read-only. |
+| Session | snapshot diff for the selected session, `directory = worktree.path` | selected local session | Highest new value: separates *this agent session's* edits from manual edits and setup-script output. |
+
+Two honest caveats to design around:
+
+- On a fresh worktree where the agent never commits (the common case), `Branch` ≈ `Staged` +
+ `Unstaged`, so the selector adds little until a commit exists. It is still worth shipping because
+ `Session` is valuable immediately and because committing agents are increasingly common.
+- A worktree can hold several sessions. Expose only the **currently selected** session's Session
+ scope; a per-session submenu is a follow-up, not v1.
+
+## UI design
+
+### Full-screen review tab
+
+Put the controls at the head of the existing left toolbar group
+(`FullScreenDiffView.tsx:542-571`), before the unified/split radio. Do **not** add a second row:
+the standalone panel's separate header row should collapse into this same slot so both hosts render
+one identical toolbar.
+
+```
++-----------------------------------------------------------------------------------------------+
+| [Branch v] feat/foo -> origin/main [Default] | (Unified|Split) 12 files +340 -88 | |
+| ^ scope ^ base picker ^ existing stats |
+| Expand all Send 3 to chat [x] |
++-----------------------------------------------------------------------------------------------+
+| tree | diff |
+```
+
+### Compact side panel
+
+`DiffPanel`'s header (`DiffPanel.tsx:476-524`) already competes for width with the radio group,
+stats, and three icon buttons in a resizable inspector. Add a **second compact row** under the
+existing header rather than cramming one row:
+
+```
++------------------------------------------+
+| Changes (Unified|Split) 12f +340 -88 | [expand] [fullscreen] [x]
+| [Branch v] -> origin/main |
++------------------------------------------+
+```
+
+Below a width threshold, drop the `-> origin/main` hint and keep only `[Branch v]`; the full base
+picker stays reachable in the full-screen tab. The side panel must at least *display* the active
+scope even when narrow, because scope state is shared with the review tab (single
+`SourceController`) and an unexplained staged-only file list is confusing.
+
+### Dropdown
+
+Reuse `DiffPickerHeader` unchanged: it already renders grouped options with per-option tooltips
+from `diffViewer.source..tooltip` (`webview-ui/src/i18n/en.ts:1235-1248`).
+
+```
++--------------------------+
+| GIT |
+| Branch | tooltip: all changes vs base, incl. local commits
+| Staged |
+| Unstaged |
+| SESSION |
+| Session |
++--------------------------+
+```
+
+Deliberately **no per-scope file counts** in the dropdown. Counts would require polling every scope
+continuously (four git pipelines per tick per worktree), which is not worth it. The Branch row's
+"vs origin/main" context is already carried by the adjacent base picker.
+
+### UX traps to close
+
+- **Apply to local always applies Branch scope.** It builds its patch from
+ `remoteRef(worktree)` regardless of what the review shows. When the active scope is not `Branch`,
+ either label the button "Apply branch changes" or disable it with a tooltip explaining that apply
+ is branch-scoped. Otherwise users will read "Apply" as "apply what I am looking at".
+- **Revert must follow source capabilities.** `staged` and `unstaged` declare
+ `capabilities.revert: false` (`staged.ts:29`, `unstaged.ts:29`). Agent Manager currently
+ hardcodes `{ revert: true, comments: true }` (`worktree-diff-controller.ts:233`). Send the real
+ descriptor capabilities and pass them into the already-existing `canRevert` / `canComment` props
+ (`FullScreenDiffView.tsx:88-91`).
+- **Scope switch should not flash stale files.** Key the webview diff store by the composite key so
+ switching back to a previously fetched scope is instant and never renders another scope's files.
+
+## Base branch picker: semantics and the consistency problem
+
+In the standalone panel the base override is ephemeral and its `Default` means
+"auto-resolved tracking or repo default" (`shared/target.ts:4-27`). In Agent Manager, `Default`
+should mean **the worktree's recorded parent** (`origin/`), which is a deliberate
+recorded value, not a guess.
+
+The real issue: as soon as a base override exists, the review toolbar and the sidebar badge can
+disagree, because `GitStatsPoller` and `Apply to local` both derive from `remoteRef(worktree)`.
+
+Two coherent options:
+
+**A. View-local ephemeral override (cheap).** Mirrors the standalone panel. Zero risk to stats,
+apply, and PR flows, but the sidebar badge will visibly disagree with the review toolbar the moment
+the user overrides. Needs the review header to always show `vs ` so the difference is legible.
+
+**B. Persisted worktree base (recommended).** Treat the picker as editing the worktree's base:
+persist `parentBranch` / `remote` (or a new `diffBase`) in `agent-manager.json`
+(`WorktreeStateManager.ts:16-45`), and let stats, review, and apply all read the same value. This
+keeps a single base per worktree with no divergence, and it fixes a real existing gap: today a
+worktree's base cannot be changed after creation, so branching off the wrong base is unrecoverable
+without recreating the worktree.
+
+Recommendation: ship **A** in the same phase as the scope selector to keep the diff small, then
+promote to **B** as its own change, because B needs stats invalidation, apply, and PR paths updated
+together and deserves isolated review. If B is chosen up front, do not also keep A: two competing
+bases is worse than either.
+
+## Reuse plan
+
+The point of this work is to add zero new diff rendering code.
+
+### Reuse as-is
+
+- `DiffPickerHeader.tsx` (scope select, grouping, tooltips)
+- `BaseBranchPicker.tsx` and `webview-ui/src/components/shared/BranchSelect.tsx`
+- `FullScreenDiffView.tsx`, `FileTree.tsx`, `VirtualDiffList.tsx`, `diff-state.ts`,
+ `diff-requests.ts`, `diff-open-policy.ts`
+- `SourceController.ts`, `sources/session.ts`, `local-diff.ts`, `GitOps.ts`
+- All `diffViewer.source.*` and `diffViewer.baseBranch.*` i18n keys, already translated
+
+### New shared pieces (small)
+
+1. `webview-ui/diff-viewer/DiffScopeControls.tsx` - composes `DiffPickerHeader` +
+ `BaseBranchPicker`, plus a `compact` flag for the side panel. Consumed by three hosts:
+ standalone `DiffViewerApp`, Agent Manager review tab, Agent Manager side panel.
+2. A leading toolbar slot prop on both renderers: `FullScreenDiffView` (`lead?: JSXElement`,
+ rendered first inside `am-review-toolbar-left`) and `DiffPanel` (second header row). Once
+ `FullScreenDiffView` has the slot, move the standalone panel's separate header row into it so
+ both hosts share one layout.
+3. `webview-ui/agent-manager/diff-scope-state.ts` - composite key helpers and per-context scope
+ signal. Keep this out of `AgentManagerApp.tsx` (already 3191 lines).
+
+### Extension side
+
+4. Generalize `PanelContext` so `workspaceRoot` is the resolved diff directory, and make
+ `DiffSourceCatalog` build sources for that directory. `SourceController.setContext` is already
+ called by Agent Manager (`worktree-diff-controller.ts:79`, `:188`), so the plumbing exists.
+5. `WorktreeDiffController.source()` delegates to `DiffSourceCatalog` instead of returning its
+ bespoke descriptor. This deletes Agent Manager's synthetic source and gets staged, unstaged, and
+ session scopes for free.
+6. `src/agent-manager/diff-scope.ts` (new, vscode-free): composite id parse/format, scope to source
+ id mapping, capability lookup. `worktree-diff-controller.ts` is already 332 lines and
+ `tests/unit/agent-manager-arch.test.ts` enforces `maxLines` caps that must not be raised.
+
+## Message and state changes
+
+Inbound (webview to extension), all additive and optional so existing callers keep working:
+
+| Message | Change |
+|---|---|
+| `agentManager.requestWorktreeDiff` | add `scope?` |
+| `agentManager.startDiffWatch` | add `scope?` |
+| `agentManager.requestWorktreeDiffFile` | add `scope?` |
+| `agentManager.revertWorktreeFile` | add `scope?` |
+| `agentManager.requestDiffBranches` | new, `{ sessionId }` |
+| `agentManager.setDiffBaseBranch` | new, `{ sessionId, branch? }` where `undefined` clears |
+
+No separate "set scope" message: a scope switch is a re-activation via `startDiffWatch` /
+`requestWorktreeDiff` with the new scope.
+
+Outbound: `worktreeDiff`, `worktreeDiffLoading`, `worktreeDiffFile`, `revertWorktreeFileResult`
+gain `scope` and the source `capabilities`; new `agentManager.diffBranches` reuses the existing
+`WorkspaceBranchesResult` shape (`sources/catalog.ts:22-33`).
+
+Webview state: `diffDatas` and `diffFileLoading` re-key from `sessionId` to `${ctx}#${scope}`;
+`diffSessionKey()` (`AgentManagerApp.tsx:1694-1699`) appends the scope so accordion open state
+resets per scope.
+
+Known breakage to fix during the refactor: `shouldStopForWorktree` passes
+`this.controller.currentId` into `shouldStopDiffPolling`, which compares it against orphaned
+session ids (`delete-worktree.ts:11-20`). Pass the parsed context id, not the composite id.
+
+Target resolution ordering: today `ensureTarget` resolves lazily inside `fetch()`. With the catalog
+owning source construction, the directory and base must be resolved *before*
+`controller.activate()`. `activate` is already awaited by `request` and `start`, so awaiting
+`ready()` plus target resolution first is safe, but this is the main refactor risk in the change.
+
+## Phasing
+
+1. **Parameterize sources by directory.** Add explicit `dir` / injected `log` / shared `GitOps` to
+ `worktree.ts`, `staged.ts`, `unstaged.ts`, and `listWorkspaceBranches`. No user-visible change;
+ standalone panel keeps passing the workspace root. Verify the standalone panel is unaffected.
+2. **Composite keying and catalog delegation.** `diff-scope.ts`, controller delegates to the
+ catalog, messages gain `scope` and `capabilities`, webview re-keys. Still one scope exposed, so
+ still no visible change. This is the risky phase and should land on its own.
+3. **Scope selector UI.** `DiffScopeControls`, toolbar slots in both hosts, standalone header moved
+ into the shared slot, `canRevert` / `canComment` wired from capabilities, Apply-scope guard.
+4. **Base picker in Agent Manager.** `requestDiffBranches` / `setDiffBaseBranch`, ephemeral
+ override (option A), `Default` labeled as the worktree's recorded parent.
+5. **Optional follow-up.** Promote the override to a persisted worktree base (option B) shared with
+ stats, apply, and PR.
+
+## Verification
+
+- `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip`
+ (new exports must be imported somewhere).
+- New unit tests: scope to source id mapping, composite id parse/format, base override resolution
+ for a worktree directory, and `shouldStopDiffPolling` with composite ids. Existing coverage to
+ keep green: `tests/unit/local-diff.test.ts`, `tests/unit/agent-manager-arch.test.ts`.
+- Visual regression stories for the toolbar in both hosts, per the `vscode-visual-regression`
+ skill.
+- Manual (self-test instance): worktree with a commit plus dirty files, switch Branch / Staged /
+ Unstaged / Session, confirm counts differ correctly, revert hidden in read-only scopes, side
+ panel and review tab stay in sync, base override changes the file set, and switching worktrees
+ resets to Branch.
+- Changeset required (user-facing feature).
+
+## Non-goals
+
+- A `Local workspace` scope comparing a worktree against the local checkout's working tree. Useful,
+ but it needs a tree-to-tree diff path that does not exist in `local-diff.ts` and it overlaps with
+ the `Apply to local` conflict check.
+- Per-session Session scope submenu when a worktree holds multiple sessions.
+- Turn scope inside Agent Manager. Per-turn changes already open the standalone panel from the
+ transcript (`VscodeSessionTurn.tsx:179-200`).
+- Per-scope file counts in the dropdown.
+- Merging the standalone Changes panel into Agent Manager. Phase 3 makes them converge visually;
+ actually collapsing the two hosts is a separate decision.
+
+## Open questions
+
+1. Option A or B for the base picker (view-local override vs persisted worktree base). B is the
+ better end state; A is the smaller step.
+2. Should the scope persist per worktree in `agent-manager.json` alongside `reviewDiffStyle`, or
+ always reset to Branch on selection change? Resetting is more predictable; persisting is less
+ repetitive for someone who lives in Session scope.
+3. Should the side panel's scope control be interactive or display-only, given how little width it
+ has?
diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
index bf72af74ab8..2556bd391db 100644
--- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
+++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
@@ -4,10 +4,12 @@ import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import { getErrorMessage } from "../kilo-provider-utils"
import { resolveLocalDiffTarget } from "../diff/shared/target"
+import { DiffSourceCatalog } from "../diff/sources/catalog"
import { getDiffMarkdownRender, setDiffMarkdownRender } from "../review-settings"
import { isAbsolutePath } from "../path-utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { remoteRef, WorktreeStateManager, type Worktree } from "./WorktreeStateManager"
+import { composeDiffId, normalizeScope } from "./diff-scope"
import { handleSection } from "./section-handler"
import { normalizeBaseBranch } from "./base-branch"
import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller"
@@ -71,6 +73,7 @@ export class AgentManagerProvider implements Disposable {
private orchestration: AgentManagerOrchestrationBridge
private gitOps: GitOps
private diffs: WorktreeDiffController
+ private diffCatalog: DiffSourceCatalog
private naming: BranchNamingController
private staleWorktreeIds = new Set()
private toolRequests = new Set()
@@ -148,12 +151,13 @@ export class AgentManagerProvider implements Disposable {
log: (msg) => this.log(msg),
})
const local = createLocalDiff(this.gitOps, (...args) => this.log(...args))
+ this.diffCatalog = new DiffSourceCatalog(this.connectionService)
this.diffs = new WorktreeDiffController({
getState: () => this.getStateManager(),
getRoot: () => this.getRoot(),
getStateReady: () => this.stateReady,
+ catalog: this.diffCatalog,
git: this.gitOps,
- localDiff: local.summary,
localDiffFile: local.file,
post: (msg) => this.postToWebview(msg),
log: (...args) => this.log(...args),
@@ -679,11 +683,11 @@ export class AgentManagerProvider implements Disposable {
private onDiffMessage(m: AgentManagerInMessage): Record | null | undefined {
if (m.type === "agentManager.requestWorktreeDiff") {
- void this.diffs.request(m.sessionId)
+ void this.diffs.request(composeDiffId(m.sessionId, normalizeScope(m.scope)))
return null
}
if (m.type === "agentManager.requestWorktreeDiffFile") {
- void this.diffs.requestFile(m.sessionId, m.file)
+ void this.diffs.requestFile(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.file)
return null
}
if (m.type === "agentManager.applyWorktreeDiff") {
@@ -691,23 +695,52 @@ export class AgentManagerProvider implements Disposable {
return null
}
if (m.type === "agentManager.revertWorktreeFile") {
- void this.diffs.revert(m.sessionId, m.file)
+ void this.diffs.revert(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.file)
return null
}
if (m.type === "agentManager.startDiffWatch") {
- this.diffs.start(m.sessionId)
+ this.diffs.start(composeDiffId(m.sessionId, normalizeScope(m.scope)))
return null
}
if (m.type === "agentManager.stopDiffWatch") {
this.diffs.stop()
return null
}
+ if (m.type === "agentManager.requestDiffBranches") {
+ void this.sendDiffBranches(m.sessionId, m.scope)
+ return null
+ }
+ if (m.type === "agentManager.setDiffBaseBranch") {
+ void this.diffs.setBase(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.branch).then(() => {
+ void this.sendDiffBranches(m.sessionId, m.scope)
+ })
+ return null
+ }
if (m.type === "agentManager.openFile") {
this.openWorktreeFile(m.sessionId, m.filePath, m.line, m.column)
return null
}
}
+ private async sendDiffBranches(sessionId: string, scope?: string): Promise {
+ const id = composeDiffId(sessionId, normalizeScope(scope))
+ const result = await this.diffs.branches(id).catch((err) => {
+ this.log("Failed to list diff branches:", err instanceof Error ? err.message : String(err))
+ return undefined
+ })
+ if (!result) return
+ this.postToWebview({
+ type: "agentManager.diffBranches",
+ sessionId: id,
+ branches: result.branches,
+ defaultBranch: result.defaultBranch,
+ autoBase: result.autoBase,
+ currentBase: result.currentBase,
+ isAuto: result.isAuto,
+ currentBranch: result.currentBranch,
+ })
+ }
+
private onBridgeMessage(m: AgentManagerInMessage): Record | null | undefined {
if (m.type !== "openFile") return undefined
@@ -1914,6 +1947,7 @@ export class AgentManagerProvider implements Disposable {
this.orchestration.dispose()
this.visiblePresence.clear()
this.diffs.stop()
+ this.diffCatalog.dispose()
this.naming.dispose()
this.statsPoller.stop()
this.gitOps.dispose()
diff --git a/packages/kilo-vscode/src/agent-manager/diff-scope.ts b/packages/kilo-vscode/src/agent-manager/diff-scope.ts
new file mode 100644
index 00000000000..21d2e007935
--- /dev/null
+++ b/packages/kilo-vscode/src/agent-manager/diff-scope.ts
@@ -0,0 +1,57 @@
+/**
+ * Composite diff-source keying for Agent Manager.
+ *
+ * Agent Manager keys diff sources by *context* (a session id, or the `local`
+ * workspace pseudo-context) while the standalone Changes viewer keys by
+ * *scope* (branch / staged / unstaged / session). To expose scopes in Agent
+ * Manager we compose the two into a single id the SourceController can build.
+ *
+ * ctx = "local" | ""
+ * scope = "branch" | "staged" | "unstaged" | "session"
+ * id = `${ctx}#${scope}`
+ *
+ * `ctx#branch` is the default and reproduces the pre-scope behavior exactly.
+ */
+
+export type DiffScope = "branch" | "staged" | "unstaged" | "session"
+
+export const DEFAULT_DIFF_SCOPE: DiffScope = "branch"
+
+const SEP = "#"
+
+export function composeDiffId(ctx: string, scope: DiffScope): string {
+ return `${ctx}${SEP}${scope}`
+}
+
+/**
+ * Split a composite id back into context and scope. Tolerates a bare context
+ * id (no separator) by assuming the default branch scope, which keeps the
+ * pre-scope messages working unchanged.
+ */
+export function parseDiffId(id: string): { ctx: string; scope: DiffScope } {
+ const idx = id.lastIndexOf(SEP)
+ if (idx === -1) return { ctx: id, scope: DEFAULT_DIFF_SCOPE }
+ const scope = id.slice(idx + SEP.length)
+ if (isDiffScope(scope)) return { ctx: id.slice(0, idx), scope }
+ return { ctx: id, scope: DEFAULT_DIFF_SCOPE }
+}
+
+export function isDiffScope(value: string): value is DiffScope {
+ return value === "branch" || value === "staged" || value === "unstaged" || value === "session"
+}
+
+export function normalizeScope(value: unknown): DiffScope {
+ return typeof value === "string" && isDiffScope(value) ? value : DEFAULT_DIFF_SCOPE
+}
+
+/**
+ * Map a scope to the underlying standalone-viewer source id the catalog knows
+ * how to build. `branch` maps to the workspace source; `session` is handled
+ * separately because it needs the session id embedded in the source id.
+ */
+export function scopeToSourceId(scope: DiffScope, ctx: string): string {
+ if (scope === "staged") return "staged"
+ if (scope === "unstaged") return "unstaged"
+ if (scope === "session") return `session:${ctx}`
+ return "workspace"
+}
diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts
index 8950c68d22f..ae72d2cd015 100644
--- a/packages/kilo-vscode/src/agent-manager/types.ts
+++ b/packages/kilo-vscode/src/agent-manager/types.ts
@@ -286,6 +286,18 @@ interface RevertWorktreeFileResultMessage {
message: string
}
+/** Branch picker data for a context's diff directory. */
+interface DiffBranchesMessage {
+ type: "agentManager.diffBranches"
+ sessionId: string
+ branches: BranchListItem[]
+ defaultBranch: string
+ autoBase?: string
+ currentBase?: string
+ isAuto: boolean
+ currentBranch?: string
+}
+
interface PRStatusOutMessage {
type: "agentManager.prStatus"
worktreeId: string
@@ -324,6 +336,7 @@ export type AgentManagerOutMessage =
| WorktreeDiffMessage
| WorktreeDiffFileMessage
| RevertWorktreeFileResultMessage
+ | DiffBranchesMessage
| PRStatusOutMessage
| ActionOutMessage
| RunStatusMessage
@@ -512,6 +525,7 @@ interface ImportFromPRIn {
interface RequestWorktreeDiffIn {
type: "agentManager.requestWorktreeDiff"
sessionId: string
+ scope?: string
}
interface ApplyWorktreeDiffIn {
@@ -524,11 +538,13 @@ interface RequestWorktreeDiffFileIn {
type: "agentManager.requestWorktreeDiffFile"
sessionId: string
file: string
+ scope?: string
}
interface StartDiffWatchIn {
type: "agentManager.startDiffWatch"
sessionId: string
+ scope?: string
}
interface StopDiffWatchIn {
@@ -539,6 +555,20 @@ interface RevertWorktreeFileIn {
type: "agentManager.revertWorktreeFile"
sessionId: string
file: string
+ scope?: string
+}
+
+interface RequestDiffBranchesIn {
+ type: "agentManager.requestDiffBranches"
+ sessionId: string
+ scope?: string
+}
+
+interface SetDiffBaseBranchIn {
+ type: "agentManager.setDiffBaseBranch"
+ sessionId: string
+ scope?: string
+ branch?: string
}
interface RefreshPRIn {
@@ -803,6 +833,8 @@ export type AgentManagerInMessage =
| StartDiffWatchIn
| StopDiffWatchIn
| RevertWorktreeFileIn
+ | RequestDiffBranchesIn
+ | SetDiffBaseBranchIn
| RefreshPRIn
| OpenPRIn
| OpenSessionsIn
diff --git a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts
index 75d538cc35c..dff233ca5cc 100644
--- a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts
+++ b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts
@@ -1,12 +1,13 @@
import { SourceController } from "../diff/SourceController"
import { resolveLocalDiffTarget } from "../diff/shared/target"
import { WorktreeDiffReverter, type StatusResolver } from "../diff/shared/reverter"
-import type { DiffFile } from "../diff/types"
-import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "../diff/sources/types"
+import type { DiffFile, PanelContext } from "../diff/types"
+import type { DiffSource } from "../diff/sources/types"
+import type { DiffSourceCatalog } from "../diff/sources/catalog"
import type { ApplyConflict, GitOps } from "./GitOps"
import { shouldStopDiffPolling } from "./delete-worktree"
-import { Semaphore } from "./semaphore"
import { remoteRef, type ManagedSession, type WorktreeStateManager } from "./WorktreeStateManager"
+import { parseDiffId, scopeToSourceId } from "./diff-scope"
import type { AgentManagerOutMessage, WorktreeDiffEntry } from "./types"
const LOCAL_DIFF_ID = "local" as const
@@ -19,14 +20,11 @@ export interface WorktreeDiffControllerContext {
getState: () => WorktreeStateManager | undefined
getRoot: () => string | undefined
getStateReady: () => Promise | undefined
- /**
- * In-process diff paths deliberately bypass the SDK client to keep git spawns
- * out of the Bun `kilo serve` process (see oven-sh/bun#18265).
- */
+ /** Builds the underlying per-scope diff sources (workspace/staged/unstaged/session). */
+ catalog: DiffSourceCatalog
+ /** Shared git ops, injected into sources so they don't spawn their own channels. */
git: GitOps
- /** In-process diff summary (replaces client.worktree.diffSummary). */
- localDiff: (dir: string, base: string) => Promise
- /** In-process single-file diff (replaces client.worktree.diffFile). */
+ /** In-process single-file diff (replaces client.worktree.diffFile). Used by revert. */
localDiffFile: (dir: string, base: string, file: string) => Promise
post: (msg: AgentManagerOutMessage) => void
log: (...args: unknown[]) => void
@@ -34,13 +32,14 @@ export interface WorktreeDiffControllerContext {
export class WorktreeDiffController {
private readonly controller: SourceController
- private readonly details = new Semaphore(3)
private target: Target | undefined
private applying: string | undefined
+ /** Ephemeral per-context base override, keyed by context id. */
+ private baseOverrides = new Map()
constructor(private readonly ctx: WorktreeDiffControllerContext) {
this.controller = new SourceController(
- (id) => this.source(id),
+ (id, ctx) => this.source(id, ctx),
() => [],
(msg) => this.ctx.post(msg as AgentManagerOutMessage),
{
@@ -80,7 +79,11 @@ export class WorktreeDiffController {
}
public shouldStopForWorktree(path: string, sessions: ManagedSession[]): boolean {
- return shouldStopDiffPolling(path, sessions, this.target, this.controller.currentId)
+ // Pass the parsed context id, not the composite id, so the orphaned-session
+ // check matches real session ids.
+ const current = this.controller.currentId
+ const ctxId = current ? parseDiffId(current).ctx : undefined
+ return shouldStopDiffPolling(path, sessions, this.target, ctxId)
}
public async apply(worktreeId: string, value?: unknown): Promise {
@@ -144,38 +147,38 @@ export class WorktreeDiffController {
}
}
- public async revert(sessionId: string, file: string): Promise {
+ public async revert(id: string, file: string): Promise {
if (!file) return
- if (this.controller.currentId !== sessionId) {
- const result = await this.revertFile(sessionId, file)
- this.postRevertResult(sessionId, file, result)
+ if (this.controller.currentId !== id) {
+ const result = await this.revertFile(id, file)
+ this.postRevertResult(id, file, result)
return
}
await this.controller.revertFile(file)
}
- public async request(sessionId: string): Promise {
- if (this.controller.currentId !== sessionId) {
- await this.activate(sessionId, false, true)
+ public async request(id: string): Promise {
+ if (this.controller.currentId !== id) {
+ await this.activate(id, false, true)
return
}
this.target = undefined
await this.controller.refresh()
}
- public async requestFile(sessionId: string, file: string): Promise {
+ public async requestFile(id: string, file: string): Promise {
if (!file) return
- if (this.controller.currentId !== sessionId) {
- this.ctx.post({ type: "agentManager.worktreeDiffFile", sessionId, file, diff: null })
+ if (this.controller.currentId !== id) {
+ this.ctx.post({ type: "agentManager.worktreeDiffFile", sessionId: id, file, diff: null })
return
}
await this.controller.requestFile(file)
}
- public start(sessionId: string): void {
- if (this.controller.isPolling && this.controller.currentId === sessionId) return
- this.ctx.log(`Starting diff polling for session ${sessionId}`)
- void this.activate(sessionId, true, true)
+ public start(id: string): void {
+ if (this.controller.isPolling && this.controller.currentId === id) return
+ this.ctx.log(`Starting diff polling for ${id}`)
+ void this.activate(id, true, true)
}
public stop(): void {
@@ -183,92 +186,113 @@ export class WorktreeDiffController {
this.target = undefined
}
- private async activate(sessionId: string, poll: boolean, fetch: boolean): Promise {
+ /**
+ * Set or clear an ephemeral base override for a context (worktree or local),
+ * then re-activate the current source so it refetches against the new base.
+ * Passing undefined clears the override and falls back to the recorded parent.
+ */
+ public async setBase(id: string, branch: string | undefined): Promise {
+ const { ctx } = parseDiffId(id)
+ if (branch) this.baseOverrides.set(ctx, branch)
+ else this.baseOverrides.delete(ctx)
this.target = undefined
- this.controller.setContext({ workspaceRoot: this.ctx.getRoot() })
- await this.controller.activate(sessionId, { poll, fetch })
+ await this.controller.reactivate()
}
- private async resolve(sessionId: string): Promise<{ directory: string; baseBranch: string } | undefined> {
- if (sessionId === LOCAL_DIFF_ID) return await this.resolveLocal()
+ /** Branch picker data for a context's directory, using any active override. */
+ public async branches(id: string) {
+ await this.ready("stateReady rejected, continuing diff branches resolve:")
+ const { ctx } = parseDiffId(id)
+ const target = await this.resolve(ctx)
+ if (!target) return undefined
+ return await this.ctx.catalog.listWorkspaceBranches(this.baseOverrides.get(ctx), target.directory)
+ }
+
+ private async activate(id: string, poll: boolean, fetch: boolean): Promise {
+ this.target = undefined
+ await this.ready("stateReady rejected, continuing diff activate:")
+ const { ctx } = parseDiffId(id)
+ const resolved = await this.resolve(ctx)
+ this.target = resolved ? { sessionId: id, ...resolved } : undefined
+ this.controller.setContext({
+ workspaceRoot: this.ctx.getRoot(),
+ dir: resolved?.directory,
+ // The resolved base already bakes in any ephemeral override (see
+ // resolve()), so pass it as the explicit base and leave
+ // baseBranchOverride unset to avoid double resolution.
+ baseBranch: resolved?.baseBranch,
+ // Agent Manager always knows its intended directory (LOCAL resolves to
+ // the root). Never fall back to the workspace root for an unresolvable
+ // worktree context — return an empty diff instead.
+ strictDir: true,
+ git: this.ctx.git,
+ log: (...args) => this.ctx.log(...args),
+ })
+ await this.controller.activate(id, { poll, fetch })
+ }
+
+ private async resolve(ctxId: string): Promise<{ directory: string; baseBranch: string } | undefined> {
+ if (ctxId === LOCAL_DIFF_ID) return await this.resolveLocal()
const state = this.ctx.getState()
if (!state) {
- this.ctx.log(`resolveDiffTarget: no state manager for session ${sessionId}`)
+ this.ctx.log(`resolveDiffTarget: no state manager for context ${ctxId}`)
return undefined
}
- const session = state.getSession(sessionId)
+ const session = state.getSession(ctxId)
if (!session) {
this.ctx.log(
- `resolveDiffTarget: session ${sessionId} not found in state (${state.getSessions().length} total sessions)`,
+ `resolveDiffTarget: session ${ctxId} not found in state (${state.getSessions().length} total sessions)`,
)
return undefined
}
if (!session.worktreeId) {
- this.ctx.log(`resolveDiffTarget: session ${sessionId} has no worktreeId (local session)`)
+ this.ctx.log(`resolveDiffTarget: session ${ctxId} has no worktreeId (local session)`)
return undefined
}
const worktree = state.getWorktree(session.worktreeId)
if (!worktree) {
- this.ctx.log(`resolveDiffTarget: worktree ${session.worktreeId} not found for session ${sessionId}`)
+ this.ctx.log(`resolveDiffTarget: worktree ${session.worktreeId} not found for session ${ctxId}`)
return undefined
}
- return { directory: worktree.path, baseBranch: remoteRef(worktree) }
+ const base = this.baseOverrides.get(ctxId) ?? remoteRef(worktree)
+ return { directory: worktree.path, baseBranch: base }
}
private async resolveLocal(): Promise<{ directory: string; baseBranch: string } | undefined> {
- return await resolveLocalDiffTarget(this.ctx.git, (...args) => this.ctx.log(...args), this.ctx.getRoot())
+ const root = this.ctx.getRoot()
+ if (!root) return undefined
+ const override = this.baseOverrides.get(LOCAL_DIFF_ID)
+ if (override) {
+ return { directory: root, baseBranch: override }
+ }
+ return await resolveLocalDiffTarget(this.ctx.git, (...args) => this.ctx.log(...args), root)
}
private async ready(msg: string): Promise {
await this.ctx.getStateReady()?.catch((err) => this.ctx.log(msg, err))
}
- private source(sessionId: string): DiffSource {
- const descriptor: DiffSourceDescriptor = {
- id: sessionId,
- type: "workspace",
- group: "Git",
- capabilities: { revert: true, comments: true },
- }
-
+ /**
+ * Build the active source for a composite id by delegating to the catalog.
+ * The composite id (ctx#scope) is preserved as the descriptor id so the
+ * webview keys diff data by context+scope. Context resolution (dir/base)
+ * already happened in activate() and is carried by the PanelContext.
+ */
+ private source(id: string, panelCtx: PanelContext): DiffSource {
+ const { ctx, scope } = parseDiffId(id)
+ const built = this.ctx.catalog.build(scopeToSourceId(scope, ctx), panelCtx)
return {
- descriptor,
- fetch: () => this.fetch(sessionId),
- fetchFile: (file) => this.fetchFile(sessionId, file),
- revert: (file) => this.revertFile(sessionId, file),
+ ...built,
+ descriptor: { ...built.descriptor, id },
}
}
- private async fetch(sessionId: string): Promise {
- await this.ready("stateReady rejected, continuing diff resolve:")
- const target = await this.ensureTarget(sessionId)
- if (!target) return { diffs: [], stopPolling: true }
-
- const files = await this.ctx.localDiff(target.directory, target.baseBranch)
- this.ctx.log(`Worktree diff returned ${files.length} file(s) for session ${sessionId}`)
- return { diffs: files as AgentManagerDiffFile[] }
- }
-
- private async fetchFile(sessionId: string, file: string): Promise {
- await this.ready("stateReady rejected, continuing diff detail resolve:")
- return this.details.run(async () => {
- const target = await this.ensureTarget(sessionId)
- if (!target) return null
-
- try {
- return (await this.ctx.localDiffFile(target.directory, target.baseBranch, file)) as AgentManagerDiffFile | null
- } catch (error) {
- this.ctx.log("Failed to fetch worktree diff file:", error)
- return null
- }
- })
- }
-
- private async revertFile(sessionId: string, file: string): Promise<{ ok: boolean; message: string }> {
+ private async revertFile(id: string, file: string): Promise<{ ok: boolean; message: string }> {
await this.ready("stateReady rejected, continuing revert resolve:")
- const target = await this.resolveTarget(sessionId)
+ const { ctx } = parseDiffId(id)
+ const target = await this.resolve(ctx)
if (!target) return { ok: false, message: "Could not resolve diff target" }
try {
@@ -285,19 +309,6 @@ export class WorktreeDiffController {
}
}
- private async ensureTarget(sessionId: string): Promise {
- if (this.controller.currentId !== sessionId) return undefined
- if (this.target?.sessionId === sessionId) return this.target
- return await this.resolveTarget(sessionId)
- }
-
- private async resolveTarget(sessionId: string): Promise {
- const target = await this.resolve(sessionId)
- if (!target) return undefined
- this.target = { sessionId, ...target }
- return this.target
- }
-
private postRevertResult(sessionId: string, file: string, result: { ok: boolean; message: string }): void {
this.ctx.post({
type: "agentManager.revertWorktreeFileResult",
diff --git a/packages/kilo-vscode/src/diff/sources/catalog.ts b/packages/kilo-vscode/src/diff/sources/catalog.ts
index fa36aaecf2d..608d4580afc 100644
--- a/packages/kilo-vscode/src/diff/sources/catalog.ts
+++ b/packages/kilo-vscode/src/diff/sources/catalog.ts
@@ -90,12 +90,13 @@ export class DiffSourceCatalog implements vscode.Disposable {
}
build(id: string, ctx: PanelContext): DiffSource {
+ const opts = { dir: () => ctx.dir, strictDir: ctx.strictDir, git: ctx.git, log: ctx.log }
if (id === WORKSPACE_SOURCE_ID) {
- return createWorktreeDiffSource({ baseBranchOverride: ctx.baseBranchOverride })
+ return createWorktreeDiffSource({ ...opts, baseBranchOverride: ctx.baseBranchOverride, baseBranch: ctx.baseBranch })
}
- if (id === STAGED_SOURCE_ID) return createStagedDiffSource()
- if (id === UNSTAGED_SOURCE_ID) return createUnstagedDiffSource()
+ if (id === STAGED_SOURCE_ID) return createStagedDiffSource(opts)
+ if (id === UNSTAGED_SOURCE_ID) return createUnstagedDiffSource(opts)
if (id.startsWith(TURN_PREFIX)) {
const [sessionId, messageId] = id.slice(TURN_PREFIX.length).split(":")
@@ -108,14 +109,14 @@ export class DiffSourceCatalog implements vscode.Disposable {
if (id.startsWith(SESSION_PREFIX)) {
const sessionId = id.slice(SESSION_PREFIX.length)
if (!sessionId) throw new Error(`DiffSourceCatalog.build: empty session id in "${id}"`)
- return createSessionDiffSource(sessionId, this.sessionFetch, ctx.workspaceRoot, this.checkSnapshotsEnabled)
+ return createSessionDiffSource(sessionId, this.sessionFetch, ctx.dir ?? ctx.workspaceRoot, this.checkSnapshotsEnabled)
}
throw new Error(`DiffSourceCatalog.build: unknown source id "${id}"`)
}
- async listWorkspaceBranches(override: string | undefined): Promise {
- const root = getWorkspaceRoot()
+ async listWorkspaceBranches(override: string | undefined, dir?: string): Promise {
+ const root = dir ?? getWorkspaceRoot()
if (!root) return undefined
const git = this.ensureBranchGit()
diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts
index d21a508902b..8cd250743cb 100644
--- a/packages/kilo-vscode/src/diff/sources/staged.ts
+++ b/packages/kilo-vscode/src/diff/sources/staged.ts
@@ -34,17 +34,39 @@ function stamp(entry: FileEntry, before: string, after: string): FileEntry {
return { ...entry, stamp: `${entry.status}:${before}:${after}` }
}
+export interface StagedDiffSourceOptions {
+ /**
+ * Resolve the directory to diff. Defaults to the VS Code workspace root.
+ * Agent Manager passes a worktree path so the source diffs inside the
+ * worktree rather than the main checkout.
+ */
+ dir?: () => string | undefined
+ /**
+ * When true, a `dir` that resolves to undefined yields an empty diff rather
+ * than falling back to the workspace root.
+ */
+ strictDir?: boolean
+ /** Shared GitOps / log so sources don't each spawn their own channel. */
+ git?: GitOps
+ log?: (...args: unknown[]) => void
+}
+
/**
* Diff between the git index and HEAD — what `git diff --cached` would show.
* Polls on the standard interval; revert isn't supported (use `git reset` from
* a real git client). Read-only view.
*/
-export function createStagedDiffSource(): DiffSource {
- const output = vscode.window.createOutputChannel("Kilo Diff: Staged")
- const log = (...args: unknown[]) => appendOutput(output, "StagedDiffSource", ...args)
- const git = new GitOps({ log })
+export function createStagedDiffSource(opts: StagedDiffSourceOptions = {}): DiffSource {
+ const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Staged")
+ const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "StagedDiffSource", ...args))
+ const git = opts.git ?? new GitOps({ log })
- const root = (): string | undefined => getWorkspaceRoot()
+ const root = (): string | undefined => {
+ const dir = opts.dir?.()
+ if (dir) return dir
+ if (opts.strictDir) return undefined
+ return getWorkspaceRoot()
+ }
const listEntries = async (dir: string): Promise => {
const [nameStatus, numstat, raw] = await Promise.all([
@@ -150,8 +172,10 @@ export function createStagedDiffSource(): DiffSource {
},
dispose(): void {
- git.dispose()
- output.dispose()
+ // Only dispose resources we own (created here). Injected git/log are
+ // owned by the caller.
+ if (!opts.git) git.dispose()
+ output?.dispose()
},
}
}
diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts
index b4d0c996161..5fcb03793d2 100644
--- a/packages/kilo-vscode/src/diff/sources/unstaged.ts
+++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts
@@ -40,17 +40,39 @@ function stamp(entry: FileEntry, before: string, after: string): FileEntry {
return { ...entry, stamp: `${entry.status}:${before}:${after}` }
}
+export interface UnstagedDiffSourceOptions {
+ /**
+ * Resolve the directory to diff. Defaults to the VS Code workspace root.
+ * Agent Manager passes a worktree path so the source diffs inside the
+ * worktree rather than the main checkout.
+ */
+ dir?: () => string | undefined
+ /**
+ * When true, a `dir` that resolves to undefined yields an empty diff rather
+ * than falling back to the workspace root.
+ */
+ strictDir?: boolean
+ /** Shared GitOps / log so sources don't each spawn their own channel. */
+ git?: GitOps
+ log?: (...args: unknown[]) => void
+}
+
/**
* Diff between the working tree and the index — what `git diff` shows for
* tracked files, plus untracked files (treated as fully-added). Read-only;
* polls on the standard interval.
*/
-export function createUnstagedDiffSource(): DiffSource {
- const output = vscode.window.createOutputChannel("Kilo Diff: Unstaged")
- const log = (...args: unknown[]) => appendOutput(output, "UnstagedDiffSource", ...args)
- const git = new GitOps({ log })
+export function createUnstagedDiffSource(opts: UnstagedDiffSourceOptions = {}): DiffSource {
+ const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Unstaged")
+ const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "UnstagedDiffSource", ...args))
+ const git = opts.git ?? new GitOps({ log })
- const root = (): string | undefined => getWorkspaceRoot()
+ const root = (): string | undefined => {
+ const dir = opts.dir?.()
+ if (dir) return dir
+ if (opts.strictDir) return undefined
+ return getWorkspaceRoot()
+ }
const listTracked = async (dir: string): Promise => {
const [nameStatus, numstat, raw] = await Promise.all([
@@ -192,8 +214,10 @@ export function createUnstagedDiffSource(): DiffSource {
},
dispose(): void {
- git.dispose()
- output.dispose()
+ // Only dispose resources we own (created here). Injected git/log are
+ // owned by the caller.
+ if (!opts.git) git.dispose()
+ output?.dispose()
},
}
}
diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts
index 1f3d8ee25a4..89e750a1c87 100644
--- a/packages/kilo-vscode/src/diff/sources/worktree.ts
+++ b/packages/kilo-vscode/src/diff/sources/worktree.ts
@@ -23,6 +23,28 @@ export interface WorktreeDiffSourceOptions {
* the current branch — only the comparison target changes. Reset on dispose.
*/
baseBranchOverride?: string
+ /**
+ * Resolve the directory to diff. Defaults to the VS Code workspace root.
+ * Agent Manager passes a worktree path so the source diffs inside the
+ * worktree rather than the main checkout.
+ */
+ dir?: () => string | undefined
+ /**
+ * When true, a `dir` that resolves to undefined yields an empty diff rather
+ * than falling back to the workspace root. Prevents an unresolvable
+ * worktree context from silently diffing the main checkout.
+ */
+ strictDir?: boolean
+ /**
+ * Explicit base branch to diff against. When set, the source skips
+ * auto-resolution (tracking → default) and diffs against this ref directly.
+ * Agent Manager passes the worktree's recorded parent so a worktree always
+ * compares against its own base even when the workspace default differs.
+ */
+ baseBranch?: string
+ /** Shared GitOps / log so sources don't each spawn their own channel. */
+ git?: GitOps
+ log?: (...args: unknown[]) => void
}
/**
@@ -32,9 +54,16 @@ export interface WorktreeDiffSourceOptions {
* extension host — no `kilo serve` round-trip.
*/
export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): DiffSource {
- const output = vscode.window.createOutputChannel("Kilo Diff: Workspace")
- const log = (...args: unknown[]) => appendOutput(output, "WorktreeDiffSource", ...args)
- const git = new GitOps({ log })
+ const output = opts.git ? undefined : vscode.window.createOutputChannel("Kilo Diff: Workspace")
+ const log = opts.log ?? ((...args: unknown[]) => appendOutput(output!, "WorktreeDiffSource", ...args))
+ const git = opts.git ?? new GitOps({ log })
+
+ const root = (): string | undefined => {
+ const dir = opts.dir?.()
+ if (dir) return dir
+ if (opts.strictDir) return undefined
+ return getWorkspaceRoot()
+ }
// Cached between fetches so repeated polling doesn't re-resolve the base
// branch every tick. Reset only on dispose (when the source is swapped out).
@@ -42,22 +71,32 @@ export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}):
const resolveTarget = async (): Promise => {
if (target) return target
+ if (opts.baseBranch) {
+ const dir = root()
+ if (!dir) {
+ log("Local diff: no directory (explicit base mode)")
+ return
+ }
+ target = { directory: dir, baseBranch: opts.baseBranch }
+ log(`Local diff: using explicit base=${opts.baseBranch} dir=${dir}`)
+ return target
+ }
if (opts.baseBranchOverride) {
- const root = getWorkspaceRoot()
- if (!root) {
+ const dir = root()
+ if (!dir) {
log("Local diff: no workspace root (override mode)")
return
}
- const resolved = await resolveOverrideRef(git, root, opts.baseBranchOverride, log)
+ const resolved = await resolveOverrideRef(git, dir, opts.baseBranchOverride, log)
if (!resolved) {
log(`Local diff: override base="${opts.baseBranchOverride}" could not be resolved, falling back to auto`)
} else {
- target = { directory: root, baseBranch: resolved }
+ target = { directory: dir, baseBranch: resolved }
log(`Local diff: using override base=${resolved}`)
return target
}
}
- target = await resolveLocalDiffTarget(git, log, getWorkspaceRoot())
+ target = await resolveLocalDiffTarget(git, log, root())
return target
}
@@ -109,8 +148,10 @@ export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}):
},
dispose(): void {
- git.dispose()
- output.dispose()
+ // Only dispose resources we own (created here). Injected git/log are
+ // owned by the caller.
+ if (!opts.git) git.dispose()
+ output?.dispose()
target = undefined
},
}
diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts
index 913d4560a31..3ff72904e13 100644
--- a/packages/kilo-vscode/src/diff/types.ts
+++ b/packages/kilo-vscode/src/diff/types.ts
@@ -10,6 +10,27 @@ export interface PanelContext {
hidePicker?: boolean
/** User-picked base branch for the workspace source. Undefined = auto. */
baseBranchOverride?: string
+ /**
+ * Explicit directory to diff inside, overriding the workspace root lookup.
+ * Agent Manager passes a worktree path so its sources operate in the
+ * worktree rather than the main checkout.
+ */
+ dir?: string
+ /**
+ * When true, a source whose `dir` resolves to undefined returns an empty
+ * diff instead of falling back to the workspace root. Agent Manager sets
+ * this so an unresolvable worktree context never silently diffs the main
+ * checkout.
+ */
+ strictDir?: boolean
+ /**
+ * Explicit base ref for the workspace source, skipping auto-resolution.
+ * Agent Manager passes the worktree's recorded parent ref.
+ */
+ baseBranch?: string
+ /** Shared GitOps / log injected by Agent Manager to avoid per-source channels. */
+ git?: import("../agent-manager/GitOps").GitOps
+ log?: (...args: unknown[]) => void
}
export type DiffImageError = "too-large" | "unreadable"
diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
index 804471f9bb0..10b88670865 100644
--- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
+++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
@@ -570,7 +570,9 @@ describe("Agent Manager Provider — onMessage routing", () => {
expect(text).toContain("class WorktreeDiffController")
expect(text).toContain("buildWorktreePatch")
expect(text).toContain("revertFile")
- expect(text).toContain("diffSummary")
+ // Summary/detail diff data comes from the shared DiffSourceCatalog sources
+ // (workspace/staged/unstaged/session), not a bespoke in-controller pipeline.
+ expect(text).toContain("catalog.build")
expect(text).toContain("shouldStopDiffPolling")
expect(providerText).toContain("this.diffs")
})
diff --git a/packages/kilo-vscode/tests/unit/diff-scope.test.ts b/packages/kilo-vscode/tests/unit/diff-scope.test.ts
new file mode 100644
index 00000000000..7714ae4fa24
--- /dev/null
+++ b/packages/kilo-vscode/tests/unit/diff-scope.test.ts
@@ -0,0 +1,52 @@
+import { describe, it, expect } from "bun:test"
+import {
+ composeDiffId,
+ parseDiffId,
+ isDiffScope,
+ normalizeScope,
+ scopeToSourceId,
+ DEFAULT_DIFF_SCOPE,
+} from "../../src/agent-manager/diff-scope"
+
+describe("diff-scope composite ids", () => {
+ it("round-trips context and scope", () => {
+ expect(parseDiffId(composeDiffId("local", "branch"))).toEqual({ ctx: "local", scope: "branch" })
+ expect(parseDiffId(composeDiffId("ses_abc", "staged"))).toEqual({ ctx: "ses_abc", scope: "staged" })
+ expect(parseDiffId(composeDiffId("ses_abc", "unstaged"))).toEqual({ ctx: "ses_abc", scope: "unstaged" })
+ expect(parseDiffId(composeDiffId("ses_abc", "session"))).toEqual({ ctx: "ses_abc", scope: "session" })
+ })
+
+ it("parses session ids containing no separator as default branch scope", () => {
+ expect(parseDiffId("ses_abc")).toEqual({ ctx: "ses_abc", scope: DEFAULT_DIFF_SCOPE })
+ })
+
+ it("treats an unknown trailing segment as part of the context, not a scope", () => {
+ // A session id that happens to contain '#' but not a valid scope keeps the
+ // full id as context and falls back to branch.
+ expect(parseDiffId("ses_a#bogus")).toEqual({ ctx: "ses_a#bogus", scope: DEFAULT_DIFF_SCOPE })
+ })
+
+ it("isDiffScope guards the closed enum", () => {
+ expect(isDiffScope("branch")).toBe(true)
+ expect(isDiffScope("staged")).toBe(true)
+ expect(isDiffScope("unstaged")).toBe(true)
+ expect(isDiffScope("session")).toBe(true)
+ expect(isDiffScope("turn")).toBe(false)
+ expect(isDiffScope("")).toBe(false)
+ })
+
+ it("normalizeScope falls back to branch for unknown input", () => {
+ expect(normalizeScope("staged")).toBe("staged")
+ expect(normalizeScope("nope")).toBe("branch")
+ expect(normalizeScope(undefined)).toBe("branch")
+ expect(normalizeScope(42)).toBe("branch")
+ })
+
+ it("maps scopes to catalog source ids", () => {
+ expect(scopeToSourceId("branch", "ses_abc")).toBe("workspace")
+ expect(scopeToSourceId("staged", "ses_abc")).toBe("staged")
+ expect(scopeToSourceId("unstaged", "ses_abc")).toBe("unstaged")
+ expect(scopeToSourceId("session", "ses_abc")).toBe("session:ses_abc")
+ expect(scopeToSourceId("branch", "local")).toBe("workspace")
+ })
+})
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
index ce6f795941b..ac86e26d1a1 100644
--- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
+++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
@@ -20,9 +20,6 @@ import type {
AgentManagerMultiVersionProgressMessage,
AgentManagerSendInitialMessage,
AgentManagerBranchesMessage,
- AgentManagerWorktreeDiffMessage,
- AgentManagerWorktreeDiffFileMessage,
- AgentManagerWorktreeDiffLoadingMessage,
AgentManagerApplyWorktreeDiffResultMessage,
AgentManagerApplyWorktreeDiffStatus,
AgentManagerApplyWorktreeDiffConflict,
@@ -156,7 +153,10 @@ import {
} from "./section-helpers"
import { sectionAwareDetector } from "./section-dnd"
import { ConstrainDragXAxis } from "./constrain-drag-x"
-import { mergeWorktreeDiffs } from "../diff-viewer/diff-state"
+import { DiffScopeControls } from "../diff-viewer/DiffScopeControls"
+import { scopeCapabilities } from "./diff-scope-state"
+import { createDiffReviewScope } from "./diff-review-scope"
+import { handleDiffMessage } from "./diff-messages"
import { initialMessage, seedInitialVariant } from "./initial-message"
import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
@@ -1491,40 +1491,13 @@ const AgentManagerContent: Component = () => {
}
}
- if (msg.type === "agentManager.worktreeDiff") {
- const ev = msg as AgentManagerWorktreeDiffMessage
- let staleFiles: Set | undefined
- setDiffDatas((prev) => {
- const existing = prev[ev.sessionId]
- const merged = existing
- ? mergeWorktreeDiffs(existing, ev.diffs)
- : { diffs: ev.diffs, stale: new Set() }
- staleFiles = merged.stale
- const next = merged.diffs
- if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev
- return { ...prev, [ev.sessionId]: next }
- })
- if (staleFiles) refreshStaleDiffs(ev.sessionId, staleFiles)
- }
-
- if (msg.type === "agentManager.worktreeDiffFile") {
- const ev = msg as AgentManagerWorktreeDiffFileMessage
- if (ev.diff) {
- setDiffDatas((prev) => {
- const existing = prev[ev.sessionId] ?? []
- const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item))
- return { ...prev, [ev.sessionId]: next }
- })
- setDiffFilePending(ev.sessionId, ev.diff.file, false)
- return
- }
- setDiffFilePending(ev.sessionId, ev.file, false)
- }
-
- if (msg.type === "agentManager.worktreeDiffLoading") {
- const ev = msg as AgentManagerWorktreeDiffLoadingMessage
- setDiffLoading(ev.loading)
- }
+ handleDiffMessage(msg, {
+ setDiffDatas,
+ setDiffFilePending,
+ setDiffLoading,
+ refreshStaleDiffs,
+ review,
+ })
if (msg.type === "agentManager.applyWorktreeDiffResult") {
const ev = msg as AgentManagerApplyWorktreeDiffResultMessage
@@ -1617,15 +1590,47 @@ const AgentManagerContent: Component = () => {
const currentDiffSessionId = createMemo(selectedDiffSessionId)
- // Start/stop diff watch when panel opens/closes, review tab opens, or session changes
+ // Diff scope + base branch state, shared by the side panel and review tab.
+ const review = createDiffReviewScope({
+ ctx: currentDiffSessionId,
+ panelOpen: diffOpen,
+ reviewActive,
+ local: LOCAL,
+ vscode,
+ })
+ // The composite id (ctx#scope) the extension keys diff data by.
+ const diffScopeId = review.id
+
+ // Shared scope + base-picker controls for the side panel and review tab.
+ const diffScopeControls = (compact: boolean) => (
+
+ )
+
+ // Start/stop diff watch when panel opens/closes, review tab opens, scope
+ // changes, or session changes.
createEffect(() => {
const panel = diffOpen()
- const review = reviewActive()
+ const active = reviewActive()
+ const scope = review.scope()
- if (panel || review) {
+ if (panel || active) {
const id = currentDiffSessionId()
if (id) {
- vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: id })
+ vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: id, scope })
return
}
vscode.postMessage({ type: "agentManager.stopDiffWatch" })
@@ -1670,33 +1675,17 @@ const AgentManagerContent: Component = () => {
tabFocus.restore()
}
- // Data for the review tab: use local diff data for local context,
- // current session for selected worktree context, or first available in that worktree.
+ // Data for the review tab / side panel: keyed by the composite diff id
+ // (ctx#scope) the extension pushes, so each scope keeps its own file set and
+ // switching back to a fetched scope is instant.
const reviewDiffs = createMemo(() => {
const data = diffDatas()
- const sel = selection()
- const id = session.currentSessionID()
- if (sel === LOCAL) return data[LOCAL] ?? []
- if (id && data[id]) {
- const current = managedSessions().find((s) => s.id === id)
- if (sel && current?.worktreeId === sel) return data[id]!
- }
- if (!sel) return []
- const ids = managedSessions()
- .filter((s) => s.worktreeId === sel)
- .map((s) => s.id)
- for (const sid of ids) {
- if (data[sid]) return data[sid]!
- }
- return []
+ const key = diffScopeId()
+ if (!key) return []
+ return data[key] ?? []
})
- const diffSessionKey = createMemo(() => {
- const sel = selection()
- if (sel === LOCAL) return `local:${LOCAL}`
- if (sel === null) return `session:${session.currentSessionID() ?? ""}`
- return `worktree:${sel}`
- })
+ const diffSessionKey = createMemo(() => diffScopeId() ?? "")
const setSharedDiffStyle = (style: "unified" | "split") => {
if (reviewDiffStyle() === style) return
@@ -1731,29 +1720,39 @@ const AgentManagerContent: Component = () => {
}
const requestDiffFile = (file: string) => {
- const sessionId = currentDiffSessionId()
- if (!sessionId) return
- if (diffFileLoading()[sessionId]?.[file]) return
- setDiffFilePending(sessionId, file, true)
- vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file })
+ const id = diffScopeId()
+ if (!id) return
+ if (diffFileLoading()[id]?.[file]) return
+ setDiffFilePending(id, file, true)
+ vscode.postMessage({
+ type: "agentManager.requestWorktreeDiffFile",
+ sessionId: currentDiffSessionId()!,
+ file,
+ scope: review.scope(),
+ })
}
- const refreshStaleDiffs = (sessionId: string, files: Set) => {
- const loading = diffFileLoading()[sessionId] ?? {}
+ const refreshStaleDiffs = (id: string, files: Set) => {
+ const loading = diffFileLoading()[id] ?? {}
for (const file of files) {
if (loading[file]) continue
- setDiffFilePending(sessionId, file, true)
- vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file })
+ setDiffFilePending(id, file, true)
+ vscode.postMessage({
+ type: "agentManager.requestWorktreeDiffFile",
+ sessionId: currentDiffSessionId()!,
+ file,
+ scope: review.scope(),
+ })
}
}
const diffFileLoadingForCurrent = createMemo(() => {
- const sessionId = currentDiffSessionId()
- if (!sessionId) return new Set()
- return new Set(Object.keys(diffFileLoading()[sessionId] ?? {}))
+ const id = diffScopeId()
+ if (!id) return new Set()
+ return new Set(Object.keys(diffFileLoading()[id] ?? {}))
})
- const revertCtl = createRevertFile(currentDiffSessionId, vscode, showToast, t)
+ const revertCtl = createRevertFile(diffScopeId, currentDiffSessionId, () => review.scope(), vscode, showToast, t)
const handleConfigureSetupScript = () => {
vscode.postMessage({ type: "agentManager.configureSetupScript" })
@@ -2755,12 +2754,19 @@ const AgentManagerContent: Component = () => {
{t("agentManager.open.button")}
-
+
@@ -3062,6 +3068,8 @@ const AgentManagerContent: Component = () => {
loadingFiles={diffFileLoadingForCurrent()}
sessionId={currentDiffSessionId()}
sessionKey={diffSessionKey()}
+ lead={diffScopeControls(true)}
+ canRevert={scopeCapabilities(review.scope()).revert}
diffStyle={reviewDiffStyle()}
onDiffStyleChange={setSharedDiffStyle}
markdownRender={markdown.render()}
@@ -3108,6 +3116,9 @@ const AgentManagerContent: Component = () => {
loadingFiles={diffFileLoadingForCurrent()}
sessionId={currentDiffSessionId()}
sessionKey={diffSessionKey()}
+ lead={diffScopeControls(false)}
+ canRevert={scopeCapabilities(review.scope()).revert}
+ canComment={scopeCapabilities(review.scope()).comments}
comments={reviewComments()}
onCommentsChange={setReviewCommentsForSelection}
composer={reviewComposer}
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx
index 1d5750caa46..29f54e37c55 100644
--- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx
+++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx
@@ -1,4 +1,4 @@
-import { type Component, createSignal, createMemo, Show, createEffect, on } from "solid-js"
+import { type Component, createSignal, createMemo, Show, createEffect, on, type JSXElement } from "solid-js"
import type { VirtualizerHandle } from "virtua/solid"
import { Diff } from "@kilocode/kilo-ui/diff"
import { Accordion } from "@kilocode/kilo-ui/accordion"
@@ -86,6 +86,10 @@ interface DiffPanelProps {
onRevertFile?: (file: string) => void
revertingFiles?: Set
activeTerminalId?: string
+ /** Optional leading row rendered under the header (e.g. the scope selector). */
+ lead?: JSXElement
+ /** Defaults to true. Hides the per-file Revert action when false. */
+ canRevert?: boolean
}
export const DiffPanel: Component = (props) => {
@@ -535,6 +539,9 @@ export const DiffPanel: Component = (props) => {
+