mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge branch 'main' into kevinvandijk/kilo-opencode-v1.2.16
This commit is contained in:
@@ -34,3 +34,7 @@ jobs:
|
||||
- name: Run unit tests
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run test:unit
|
||||
|
||||
- name: Check for dead code (knip)
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run knip
|
||||
|
||||
@@ -10,6 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matched: ${{ steps.filter.outputs.matched }}
|
||||
is_fork: ${{ steps.fork-check.outputs.is_fork }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Kilo-Org/paths-filter@master
|
||||
@@ -25,6 +26,14 @@ jobs:
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
- name: Check if PR is from a fork
|
||||
id: fork-check
|
||||
run: |
|
||||
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
visual-regression:
|
||||
needs: check-paths
|
||||
@@ -34,13 +43,20 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout (internal)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Checkout (fork)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@@ -90,7 +106,22 @@ jobs:
|
||||
CI: true
|
||||
PLAYWRIGHT_WORKERS: "4"
|
||||
|
||||
- name: Check for baseline changes (fork PRs)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
run: |
|
||||
git add packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No visual regression detected."
|
||||
else
|
||||
echo "::error::Visual regression detected. Screenshot baselines have changed."
|
||||
echo "::error::Since this PR is from a fork, updated screenshots cannot be committed automatically."
|
||||
echo "::error::Please ask a Kilo developer for help updating the screenshots."
|
||||
git diff --cached --stat
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Commit and push new baselines (if any)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
id: commit-baselines
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BOT_PAT }}
|
||||
@@ -109,7 +140,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Fail if baselines changed
|
||||
if: steps.commit-baselines.outputs.changed == 'true'
|
||||
if: needs.check-paths.outputs.is_fork != 'true' && steps.commit-baselines.outputs.changed == 'true'
|
||||
run: |
|
||||
echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review."
|
||||
exit 1
|
||||
@@ -130,13 +161,20 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout (internal)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Checkout (fork)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@@ -186,7 +224,22 @@ jobs:
|
||||
CI: true
|
||||
PLAYWRIGHT_WORKERS: "4"
|
||||
|
||||
- name: Check for baseline changes (fork PRs)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
run: |
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No visual regression detected."
|
||||
else
|
||||
echo "::error::Visual regression detected. Screenshot baselines have changed."
|
||||
echo "::error::Since this PR is from a fork, updated screenshots cannot be committed automatically."
|
||||
echo "::error::Please ask a Kilo developer for help updating the screenshots."
|
||||
git diff --cached --stat
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Commit and push new baselines (if any)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
id: commit-baselines-vscode
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BOT_PAT }}
|
||||
@@ -205,7 +258,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Fail if baselines changed
|
||||
if: steps.commit-baselines-vscode.outputs.changed == 'true'
|
||||
if: needs.check-paths.outputs.is_fork != 'true' && steps.commit-baselines-vscode.outputs.changed == 'true'
|
||||
run: |
|
||||
echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review."
|
||||
exit 1
|
||||
|
||||
Vendored
+4
-1
@@ -114,7 +114,10 @@
|
||||
"clear": false
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode"
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode",
|
||||
"env": {
|
||||
"VSCODE_EXEC_PATH": "${execPath}"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772091128,
|
||||
"narHash": "sha256-TnrYykX8Mf/Ugtkix6V+PjW7miU2yClA6uqWl/v6KWM=",
|
||||
"lastModified": 1772956932,
|
||||
"narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3f0336406035444b4a24b942788334af5f906259",
|
||||
"rev": "608d0cadfed240589a7eea422407a547ad626a14",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-7YoMJRkLfE+49GuItkoTLj1nk8CxLAHGIdRtH2L5n4w=",
|
||||
"aarch64-linux": "sha256-A+JUa7PI+ICd1+xCzQM11zZQMzsOWYFwULTo7sk4t5Q=",
|
||||
"aarch64-darwin": "sha256-Pbh4id/Crsy6s6J2s7QlbDnUwtsdUXGjt+K5qz7o1mU=",
|
||||
"x86_64-darwin": "sha256-hSdwk2LNlSr25p4YKb5Yh4K3L0CazFQ+SOXIfFY0jCY="
|
||||
"x86_64-linux": "sha256-2SnZbJPIvgCkb4jNh49CnbDAOpghgO9XihSiTlL6rYI=",
|
||||
"aarch64-linux": "sha256-HV/Sm4rBAZPjxG+SQJ95634yTyJXDjlSeYemMYg8eCE=",
|
||||
"aarch64-darwin": "sha256-PAJSd1VkNc7LUSrOAeG+rmYUw8VefR8GfkAIy8H3iV0=",
|
||||
"x86_64-darwin": "sha256-Qdk8nu4FoGjp6+7Ji5vuHw2HhM1Z+ivE7meqJivTiHk="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -108,6 +108,6 @@
|
||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
},
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -104,6 +104,7 @@ export const dict = {
|
||||
"dialog.model.empty": "لا توجد نتائج للنماذج",
|
||||
"dialog.model.manage": "إدارة النماذج",
|
||||
"dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.",
|
||||
"dialog.model.manage.provider.toggle": "تبديل جميع موديلات {{provider}}",
|
||||
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
|
||||
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
|
||||
@@ -444,6 +445,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
||||
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
||||
"session.review.noChanges": "لا توجد تغييرات",
|
||||
"session.review.noVcs": "لم يتم اكتشاف نظام VCS لـ git، لذلك لن يتم اكتشاف تغييرات الجلسة",
|
||||
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
||||
"session.files.all": "كل الملفات",
|
||||
"session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)",
|
||||
@@ -456,6 +458,10 @@ export const dict = {
|
||||
"session.todo.title": "المهام",
|
||||
"session.todo.collapse": "طي",
|
||||
"session.todo.expand": "توسيع",
|
||||
"session.modeSwitch.switching": "جارٍ التبديل إلى وضع {{mode}}…",
|
||||
"session.modeSwitch.waiting": "في انتظار اكتمال المهمة الحالية",
|
||||
"session.modeSwitch.notAvailable": "الوكيل غير متاح",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" غير موجود، يتم استخدام "{{actual}}"',
|
||||
"session.new.worktree.main": "الفرع الرئيسي",
|
||||
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
||||
"session.new.worktree.create": "إنشاء شجرة عمل جديدة",
|
||||
@@ -541,6 +547,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "تخصيص سمة Kilo.",
|
||||
"settings.general.row.font.title": "الخط",
|
||||
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
||||
"settings.general.row.reasoningSummaries.title": "إظهار ملخصات التفكير",
|
||||
"settings.general.row.reasoningSummaries.description": "عرض ملخصات تفكير النموذج في الجدول الزمني",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"إظهار أجزاء أداة shell موسعة بشكل افتراضي في الشريط الزمني",
|
||||
|
||||
@@ -104,6 +104,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Nenhum resultado de modelo",
|
||||
"dialog.model.manage": "Gerenciar modelos",
|
||||
"dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.",
|
||||
"dialog.model.manage.provider.toggle": "Alternar todos os modelos {{provider}}",
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
|
||||
"dialog.provider.viewAll": "Ver mais provedores",
|
||||
@@ -447,6 +448,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Carregando alterações...",
|
||||
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
||||
"session.review.noChanges": "Sem alterações",
|
||||
"session.review.noVcs": "Nenhum VCS git detectado, portanto as alterações da sessão não serão detectadas",
|
||||
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||
"session.files.all": "Todos os arquivos",
|
||||
"session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)",
|
||||
@@ -459,6 +461,10 @@ export const dict = {
|
||||
"session.todo.title": "Tarefas",
|
||||
"session.todo.collapse": "Recolher",
|
||||
"session.todo.expand": "Expandir",
|
||||
"session.modeSwitch.switching": "Alternando para o modo {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Aguardando a conclusão da tarefa atual",
|
||||
"session.modeSwitch.notAvailable": "Agente não disponível",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" não encontrado, usando "{{actual}}"',
|
||||
"session.new.worktree.main": "Branch principal",
|
||||
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
||||
"session.new.worktree.create": "Criar novo worktree",
|
||||
@@ -547,6 +553,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personalize como o Kilo é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte",
|
||||
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
||||
"settings.general.row.reasoningSummaries.title": "Mostrar resumos de raciocínio",
|
||||
"settings.general.row.reasoningSummaries.description": "Exibir resumos de raciocínio do modelo na linha do tempo",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Mostrar partes da ferramenta shell expandidas por padrão na linha do tempo",
|
||||
|
||||
@@ -113,6 +113,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Nema rezultata za modele",
|
||||
"dialog.model.manage": "Upravljaj modelima",
|
||||
"dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.",
|
||||
"dialog.model.manage.provider.toggle": "Uključi/isključi sve {{provider}} modele",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
|
||||
@@ -499,6 +500,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Učitavanje izmjena...",
|
||||
"session.review.empty": "Još nema izmjena u ovoj sesiji",
|
||||
"session.review.noChanges": "Nema izmjena",
|
||||
"session.review.noVcs": "Nije otkriven git VCS, stoga promjene sesije neće biti detektovane",
|
||||
|
||||
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
||||
"session.files.all": "Sve datoteke",
|
||||
@@ -515,6 +517,11 @@ export const dict = {
|
||||
"session.todo.collapse": "Sažmi",
|
||||
"session.todo.expand": "Proširi",
|
||||
|
||||
"session.modeSwitch.switching": "Prebacivanje u {{mode}} način rada…",
|
||||
"session.modeSwitch.waiting": "Čekanje na završetak trenutnog zadatka",
|
||||
"session.modeSwitch.notAvailable": "Agent nije dostupan",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" nije pronađen, koristi se "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "Glavna grana",
|
||||
"session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})",
|
||||
"session.new.worktree.create": "Kreiraj novi worktree",
|
||||
@@ -612,6 +619,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Prilagodi temu Kilo-a.",
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
|
||||
"settings.general.row.reasoningSummaries.title": "Prikaži sažetke razmišljanja",
|
||||
"settings.general.row.reasoningSummaries.description": "Prikaži sažetke razmišljanja modela u vremenskoj liniji",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
@@ -113,6 +113,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Ingen modeller fundet",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.",
|
||||
"dialog.model.manage.provider.toggle": "Skift alle {{provider}}-modeller",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
||||
@@ -496,6 +497,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||
"session.review.noChanges": "Ingen ændringer",
|
||||
"session.review.noVcs": "Ingen git VCS registreret, så sessionsændringer vil ikke blive registreret",
|
||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
|
||||
@@ -510,6 +512,11 @@ export const dict = {
|
||||
"session.todo.collapse": "Skjul",
|
||||
"session.todo.expand": "Udvid",
|
||||
|
||||
"session.modeSwitch.switching": "Skifter til {{mode}}-tilstand…",
|
||||
"session.modeSwitch.waiting": "Venter på at den aktuelle opgave er fuldført",
|
||||
"session.modeSwitch.notAvailable": "Agent ikke tilgængelig",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" ikke fundet, bruger "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opret nyt worktree",
|
||||
@@ -607,6 +614,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Tilpas hvordan Kilo er temabestemt.",
|
||||
"settings.general.row.font.title": "Skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||
"settings.general.row.reasoningSummaries.title": "Vis ræsonneringssammendrag",
|
||||
"settings.general.row.reasoningSummaries.description": "Vis modelræsonneringssammendrag i tidslinjen",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Udvid shell-værktøjsdele",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-værktøjsdele udvidet som standard i tidslinjen",
|
||||
|
||||
@@ -108,6 +108,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Keine Modellergebnisse",
|
||||
"dialog.model.manage": "Modelle verwalten",
|
||||
"dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.",
|
||||
"dialog.model.manage.provider.toggle": "Alle {{provider}}-Modelle umschalten",
|
||||
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
|
||||
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
|
||||
@@ -455,6 +456,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Lade Änderungen...",
|
||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||
"session.review.noChanges": "Keine Änderungen",
|
||||
"session.review.noVcs": "Kein Git-VCS erkannt, daher werden Sitzungsänderungen nicht erkannt",
|
||||
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||
"session.files.all": "Alle Dateien",
|
||||
"session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)",
|
||||
@@ -467,6 +469,10 @@ export const dict = {
|
||||
"session.todo.title": "Aufgaben",
|
||||
"session.todo.collapse": "Einklappen",
|
||||
"session.todo.expand": "Ausklappen",
|
||||
"session.modeSwitch.switching": "Wechsle zu {{mode}}-Modus…",
|
||||
"session.modeSwitch.waiting": "Warte auf Abschluss der aktuellen Aufgabe",
|
||||
"session.modeSwitch.notAvailable": "Agent nicht verfügbar",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" nicht gefunden, verwende "{{actual}}"',
|
||||
"session.new.worktree.main": "Haupt-Branch",
|
||||
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
||||
"session.new.worktree.create": "Neuen Worktree erstellen",
|
||||
@@ -556,6 +562,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Das Thema von Kilo anpassen.",
|
||||
"settings.general.row.font.title": "Schriftart",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
||||
"settings.general.row.reasoningSummaries.title": "Denk-Zusammenfassungen anzeigen",
|
||||
"settings.general.row.reasoningSummaries.description": "Modell-Denk-Zusammenfassungen in der Zeitleiste anzeigen",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Shell-Tool-Abschnitte ausklappen",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Shell-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||
|
||||
@@ -113,6 +113,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Sin resultados de modelos",
|
||||
"dialog.model.manage": "Gestionar modelos",
|
||||
"dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.",
|
||||
"dialog.model.manage.provider.toggle": "Alternar todos los modelos de {{provider}}",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
||||
@@ -500,6 +501,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Cargando cambios...",
|
||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||
"session.review.noChanges": "Sin cambios",
|
||||
"session.review.noVcs": "No se detectó VCS de git, por lo que los cambios de sesión no se detectarán",
|
||||
|
||||
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||
"session.files.all": "Todos los archivos",
|
||||
@@ -516,6 +518,11 @@ export const dict = {
|
||||
"session.todo.collapse": "Contraer",
|
||||
"session.todo.expand": "Expandir",
|
||||
|
||||
"session.modeSwitch.switching": "Cambiando al modo {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Esperando que la tarea actual se complete",
|
||||
"session.modeSwitch.notAvailable": "Agente no disponible",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" no encontrado, usando "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "Rama principal",
|
||||
"session.new.worktree.mainWithBranch": "Rama principal ({{branch}})",
|
||||
"session.new.worktree.create": "Crear nuevo árbol de trabajo",
|
||||
@@ -615,6 +622,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personaliza el tema de Kilo.",
|
||||
"settings.general.row.font.title": "Fuente",
|
||||
"settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código",
|
||||
"settings.general.row.reasoningSummaries.title": "Mostrar resúmenes de razonamiento",
|
||||
"settings.general.row.reasoningSummaries.description": "Mostrar resúmenes de razonamiento del modelo en la línea de tiempo",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes de la herramienta shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
@@ -104,6 +104,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Aucun résultat de modèle",
|
||||
"dialog.model.manage": "Gérer les modèles",
|
||||
"dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.",
|
||||
"dialog.model.manage.provider.toggle": "Activer/désactiver tous les modèles {{provider}}",
|
||||
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
|
||||
"dialog.provider.viewAll": "Voir plus de fournisseurs",
|
||||
@@ -451,6 +452,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Chargement des modifications...",
|
||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||
"session.review.noChanges": "Aucune modification",
|
||||
"session.review.noVcs": "Aucun VCS git détecté, les modifications de session ne seront donc pas détectées",
|
||||
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
||||
"session.files.all": "Tous les fichiers",
|
||||
"session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)",
|
||||
@@ -463,6 +465,10 @@ export const dict = {
|
||||
"session.todo.title": "Tâches",
|
||||
"session.todo.collapse": "Réduire",
|
||||
"session.todo.expand": "Développer",
|
||||
"session.modeSwitch.switching": "Passage en mode {{mode}}…",
|
||||
"session.modeSwitch.waiting": "En attente de la fin de la tâche en cours",
|
||||
"session.modeSwitch.notAvailable": "Agent non disponible",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" introuvable, utilisation de "{{actual}}"',
|
||||
"session.new.worktree.main": "Branche principale",
|
||||
"session.new.worktree.mainWithBranch": "Branche principale ({{branch}})",
|
||||
"session.new.worktree.create": "Créer un nouvel arbre de travail",
|
||||
@@ -553,6 +559,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'Kilo.",
|
||||
"settings.general.row.font.title": "Police",
|
||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||
"settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement",
|
||||
"settings.general.row.reasoningSummaries.description": "Afficher les résumés de raisonnement du modèle dans la chronologie",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Afficher les parties de l'outil shell développées par défaut dans la chronologie",
|
||||
|
||||
@@ -104,6 +104,7 @@ export const dict = {
|
||||
"dialog.model.empty": "モデルが見つかりません",
|
||||
"dialog.model.manage": "モデルを管理",
|
||||
"dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。",
|
||||
"dialog.model.manage.provider.toggle": "{{provider}} のすべてのモデルを切り替え",
|
||||
"dialog.model.unpaid.freeModels.title": "Kiloが提供する無料モデル",
|
||||
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
|
||||
"dialog.provider.viewAll": "さらにプロバイダーを表示",
|
||||
@@ -445,6 +446,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "変更を読み込み中...",
|
||||
"session.review.empty": "このセッションでの変更はまだありません",
|
||||
"session.review.noChanges": "変更なし",
|
||||
"session.review.noVcs": "git VCS が検出されなかったため、セッションの変更は検出されません",
|
||||
"session.files.selectToOpen": "開くファイルを選択",
|
||||
"session.files.all": "すべてのファイル",
|
||||
"session.files.binaryContent": "バイナリファイル(内容を表示できません)",
|
||||
@@ -457,6 +459,10 @@ export const dict = {
|
||||
"session.todo.title": "ToDo",
|
||||
"session.todo.collapse": "折りたたむ",
|
||||
"session.todo.expand": "展開",
|
||||
"session.modeSwitch.switching": "{{mode}} モードに切り替え中…",
|
||||
"session.modeSwitch.waiting": "現在のタスクの完了を待っています",
|
||||
"session.modeSwitch.notAvailable": "エージェントは利用できません",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" が見つかりません。"{{actual}}" を使用します',
|
||||
"session.new.worktree.main": "メインブランチ",
|
||||
"session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})",
|
||||
"session.new.worktree.create": "新しいワークツリーを作成",
|
||||
@@ -545,6 +551,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Kiloのテーマをカスタマイズします。",
|
||||
"settings.general.row.font.title": "フォント",
|
||||
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
||||
"settings.general.row.reasoningSummaries.title": "推論サマリーを表示",
|
||||
"settings.general.row.reasoningSummaries.description": "タイムラインにモデルの推論サマリーを表示する",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "shell ツールパーツを展開",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"タイムラインで shell ツールパーツをデフォルトで展開して表示します",
|
||||
|
||||
@@ -108,6 +108,7 @@ export const dict = {
|
||||
"dialog.model.empty": "모델 결과 없음",
|
||||
"dialog.model.manage": "모델 관리",
|
||||
"dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정",
|
||||
"dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 전환",
|
||||
"dialog.model.unpaid.freeModels.title": "Kilo에서 제공하는 무료 모델",
|
||||
"dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
|
||||
"dialog.provider.viewAll": "더 많은 공급자 보기",
|
||||
@@ -447,6 +448,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "변경 사항 로드 중...",
|
||||
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
||||
"session.review.noChanges": "변경 없음",
|
||||
"session.review.noVcs": "git VCS가 감지되지 않아 세션 변경 사항이 감지되지 않습니다",
|
||||
"session.files.selectToOpen": "열 파일을 선택하세요",
|
||||
"session.files.all": "모든 파일",
|
||||
"session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)",
|
||||
@@ -459,6 +461,10 @@ export const dict = {
|
||||
"session.todo.title": "할 일",
|
||||
"session.todo.collapse": "접기",
|
||||
"session.todo.expand": "펼치기",
|
||||
"session.modeSwitch.switching": "{{mode}} 모드로 전환 중…",
|
||||
"session.modeSwitch.waiting": "현재 작업이 완료될 때까지 대기 중",
|
||||
"session.modeSwitch.notAvailable": "에이전트를 사용할 수 없음",
|
||||
"session.modeSwitch.fallback": '"{{requested}}"을(를) 찾을 수 없어 "{{actual}}"을(를) 사용합니다',
|
||||
"session.new.worktree.main": "메인 브랜치",
|
||||
"session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})",
|
||||
"session.new.worktree.create": "새 작업 트리 생성",
|
||||
@@ -546,6 +552,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Kilo 테마 사용자 지정",
|
||||
"settings.general.row.font.title": "글꼴",
|
||||
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
||||
"settings.general.row.reasoningSummaries.title": "추론 요약 표시",
|
||||
"settings.general.row.reasoningSummaries.description": "타임라인에 모델 추론 요약 표시",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "shell 도구 파트 펼치기",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"타임라인에서 기본적으로 shell 도구 파트를 펼친 상태로 표시합니다",
|
||||
|
||||
@@ -116,6 +116,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Ingen modellresultater",
|
||||
"dialog.model.manage": "Administrer modeller",
|
||||
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
|
||||
"dialog.model.manage.provider.toggle": "Veksle alle {{provider}}-modeller",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
||||
@@ -500,6 +501,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Laster endringer...",
|
||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||
"session.review.noChanges": "Ingen endringer",
|
||||
"session.review.noVcs": "Ingen git VCS oppdaget, så øktendringer vil ikke bli oppdaget",
|
||||
|
||||
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||
"session.files.all": "Alle filer",
|
||||
@@ -516,6 +518,11 @@ export const dict = {
|
||||
"session.todo.collapse": "Skjul",
|
||||
"session.todo.expand": "Utvid",
|
||||
|
||||
"session.modeSwitch.switching": "Bytter til {{mode}}-modus…",
|
||||
"session.modeSwitch.waiting": "Venter på at gjeldende oppgave er ferdig",
|
||||
"session.modeSwitch.notAvailable": "Agent ikke tilgjengelig",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" ikke funnet, bruker "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "Hovedgren",
|
||||
"session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})",
|
||||
"session.new.worktree.create": "Opprett nytt worktree",
|
||||
@@ -615,6 +622,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Tilpass hvordan Kilo er tematisert.",
|
||||
"settings.general.row.font.title": "Skrift",
|
||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||
"settings.general.row.reasoningSummaries.title": "Vis resonneringssammendrag",
|
||||
"settings.general.row.reasoningSummaries.description": "Vis modellressonneringssammendrag i tidslinjen",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen",
|
||||
|
||||
@@ -104,6 +104,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Brak wyników modelu",
|
||||
"dialog.model.manage": "Zarządzaj modelami",
|
||||
"dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.",
|
||||
"dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}",
|
||||
"dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
|
||||
"dialog.provider.viewAll": "Zobacz więcej dostawców",
|
||||
@@ -446,6 +447,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Ładowanie zmian...",
|
||||
"session.review.empty": "Brak zmian w tej sesji",
|
||||
"session.review.noChanges": "Brak zmian",
|
||||
"session.review.noVcs": "Nie wykryto git VCS, więc zmiany sesji nie będą wykrywane",
|
||||
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
||||
"session.files.all": "Wszystkie pliki",
|
||||
"session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)",
|
||||
@@ -458,6 +460,10 @@ export const dict = {
|
||||
"session.todo.title": "Zadania",
|
||||
"session.todo.collapse": "Zwiń",
|
||||
"session.todo.expand": "Rozwiń",
|
||||
"session.modeSwitch.switching": "Przełączanie do trybu {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Oczekiwanie na ukończenie bieżącego zadania",
|
||||
"session.modeSwitch.notAvailable": "Agent niedostępny",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" nie znaleziono, używam "{{actual}}"',
|
||||
"session.new.worktree.main": "Główna gałąź",
|
||||
"session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})",
|
||||
"session.new.worktree.create": "Utwórz nowe drzewo robocze",
|
||||
@@ -546,6 +552,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Dostosuj motyw Kilo.",
|
||||
"settings.general.row.font.title": "Czcionka",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
||||
"settings.general.row.reasoningSummaries.title": "Pokaż podsumowania rozumowania",
|
||||
"settings.general.row.reasoningSummaries.description": "Wyświetlaj podsumowania rozumowania modelu na osi czasu",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Rozwijaj elementy narzędzia shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
"Domyślnie pokazuj rozwinięte elementy narzędzia shell na osi czasu",
|
||||
|
||||
@@ -113,6 +113,7 @@ export const dict = {
|
||||
"dialog.model.empty": "Модели не найдены",
|
||||
"dialog.model.manage": "Управление моделями",
|
||||
"dialog.model.manage.description": "Настройте какие модели появляются в выборе модели",
|
||||
"dialog.model.manage.provider.toggle": "Переключить все модели {{provider}}",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
|
||||
@@ -500,6 +501,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "Загрузка изменений...",
|
||||
"session.review.empty": "Изменений в этой сессии пока нет",
|
||||
"session.review.noChanges": "Нет изменений",
|
||||
"session.review.noVcs": "Git VCS не обнаружен, поэтому изменения сеанса не будут отслеживаться",
|
||||
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
||||
"session.files.all": "Все файлы",
|
||||
"session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)",
|
||||
@@ -514,6 +516,11 @@ export const dict = {
|
||||
"session.todo.collapse": "Свернуть",
|
||||
"session.todo.expand": "Развернуть",
|
||||
|
||||
"session.modeSwitch.switching": "Переключение в режим {{mode}}…",
|
||||
"session.modeSwitch.waiting": "Ожидание завершения текущей задачи",
|
||||
"session.modeSwitch.notAvailable": "Агент недоступен",
|
||||
"session.modeSwitch.fallback": '"{{requested}}" не найден, используется "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "Основная ветка",
|
||||
"session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})",
|
||||
"session.new.worktree.create": "Создать новый worktree",
|
||||
@@ -613,6 +620,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "Настройте оформление Kilo.",
|
||||
"settings.general.row.font.title": "Шрифт",
|
||||
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
||||
"settings.general.row.reasoningSummaries.title": "Показывать сводки рассуждений",
|
||||
"settings.general.row.reasoningSummaries.description": "Отображать сводки рассуждений модели в хронологии",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description":
|
||||
|
||||
@@ -113,6 +113,7 @@ export const dict = {
|
||||
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
|
||||
"dialog.model.manage": "จัดการโมเดล",
|
||||
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
|
||||
"dialog.model.manage.provider.toggle": "สลับโมเดล {{provider}} ทั้งหมด",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
|
||||
@@ -495,6 +496,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
|
||||
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
|
||||
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
|
||||
"session.review.noVcs": "ไม่พบ git VCS ดังนั้นจะไม่สามารถตรวจจับการเปลี่ยนแปลงของเซสชันได้",
|
||||
|
||||
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
||||
"session.files.all": "ไฟล์ทั้งหมด",
|
||||
@@ -511,6 +513,11 @@ export const dict = {
|
||||
"session.todo.collapse": "ย่อ",
|
||||
"session.todo.expand": "ขยาย",
|
||||
|
||||
"session.modeSwitch.switching": "กำลังสลับไปยังโหมด {{mode}}…",
|
||||
"session.modeSwitch.waiting": "รอให้งานปัจจุบันเสร็จสมบูรณ์",
|
||||
"session.modeSwitch.notAvailable": "ตัวแทนไม่พร้อมใช้งาน",
|
||||
"session.modeSwitch.fallback": 'ไม่พบ "{{requested}}" กำลังใช้ "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "สาขาหลัก",
|
||||
"session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})",
|
||||
"session.new.worktree.create": "สร้าง worktree ใหม่",
|
||||
@@ -607,6 +614,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม",
|
||||
"settings.general.row.font.title": "ฟอนต์",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
|
||||
"settings.general.row.reasoningSummaries.title": "แสดงสรุปการให้เหตุผล",
|
||||
"settings.general.row.reasoningSummaries.description": "แสดงสรุปการให้เหตุผลของโมเดลในไทม์ไลน์",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||
|
||||
@@ -140,6 +140,7 @@ export const dict = {
|
||||
"dialog.model.empty": "未找到模型",
|
||||
"dialog.model.manage": "管理模型",
|
||||
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
|
||||
"dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型",
|
||||
"dialog.model.unpaid.freeModels.title": "Kilo 提供的免费模型",
|
||||
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
||||
|
||||
@@ -498,6 +499,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "正在加载更改...",
|
||||
"session.review.empty": "此会话暂无更改",
|
||||
"session.review.noChanges": "无更改",
|
||||
"session.review.noVcs": "未检测到 git VCS,因此无法检测到会话更改",
|
||||
"session.files.selectToOpen": "选择要打开的文件",
|
||||
"session.files.all": "所有文件",
|
||||
"session.files.binaryContent": "二进制文件(无法显示内容)",
|
||||
@@ -510,6 +512,10 @@ export const dict = {
|
||||
"session.todo.title": "待办事项",
|
||||
"session.todo.collapse": "折叠",
|
||||
"session.todo.expand": "展开",
|
||||
"session.modeSwitch.switching": "正在切换到 {{mode}} 模式…",
|
||||
"session.modeSwitch.waiting": "等待当前任务完成",
|
||||
"session.modeSwitch.notAvailable": "Agent 不可用",
|
||||
"session.modeSwitch.fallback": '找不到 "{{requested}}",使用 "{{actual}}"',
|
||||
"session.new.worktree.main": "主分支",
|
||||
"session.new.worktree.mainWithBranch": "主分支({{branch}})",
|
||||
"session.new.worktree.create": "创建新的 worktree",
|
||||
@@ -607,6 +613,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "自定义 Kilo 的主题。",
|
||||
"settings.general.row.font.title": "字体",
|
||||
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
||||
"settings.general.row.reasoningSummaries.title": "显示推理摘要",
|
||||
"settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "展开 shell 工具部分",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||
|
||||
@@ -117,6 +117,7 @@ export const dict = {
|
||||
"dialog.model.empty": "找不到模型",
|
||||
"dialog.model.manage": "管理模型",
|
||||
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
|
||||
"dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Kilo 提供的免費模型",
|
||||
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
|
||||
@@ -493,6 +494,7 @@ export const dict = {
|
||||
"session.review.loadingChanges": "正在載入變更...",
|
||||
"session.review.empty": "此工作階段暫無變更",
|
||||
"session.review.noChanges": "沒有變更",
|
||||
"session.review.noVcs": "未偵測到 git VCS,因此無法偵測到工作階段變更",
|
||||
"session.files.selectToOpen": "選取要開啟的檔案",
|
||||
"session.files.all": "所有檔案",
|
||||
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
|
||||
@@ -507,6 +509,11 @@ export const dict = {
|
||||
"session.todo.collapse": "折疊",
|
||||
"session.todo.expand": "展開",
|
||||
|
||||
"session.modeSwitch.switching": "正在切換到 {{mode}} 模式…",
|
||||
"session.modeSwitch.waiting": "等待目前任務完成",
|
||||
"session.modeSwitch.notAvailable": "Agent 不可用",
|
||||
"session.modeSwitch.fallback": '找不到 "{{requested}}",使用 "{{actual}}"',
|
||||
|
||||
"session.new.worktree.main": "主分支",
|
||||
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
|
||||
"session.new.worktree.create": "建立新的 worktree",
|
||||
@@ -602,6 +609,8 @@ export const dict = {
|
||||
"settings.general.row.theme.description": "自訂 Kilo 的主題。",
|
||||
"settings.general.row.font.title": "字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
||||
"settings.general.row.reasoningSummaries.title": "顯示推理摘要",
|
||||
"settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要",
|
||||
|
||||
"settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊",
|
||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.0.40"
|
||||
version = "7.0.43"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.40/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.43/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.40/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.43/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.40/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.43/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.40/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.43/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.40/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.43/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -16,6 +16,7 @@ const sectionNavItems: SectionNav = {
|
||||
contributing: Nav.ContributingNav,
|
||||
"ai-providers": Nav.AiProvidersNav,
|
||||
gateway: Nav.GatewayNav,
|
||||
kiloclaw: Nav.KiloClawNav,
|
||||
}
|
||||
|
||||
// Main nav items with their section keys
|
||||
@@ -28,6 +29,7 @@ const mainNavItems = [
|
||||
{ label: "Automate", href: "/automate", sectionKey: "automate" },
|
||||
{ label: "Deploy & Secure", href: "/deploy-secure", sectionKey: "deploy-secure" },
|
||||
{ label: "AI Gateway", href: "/gateway", sectionKey: "gateway" },
|
||||
{ label: "KiloClaw", href: "/kiloclaw", sectionKey: "kiloclaw" },
|
||||
{ label: "Contributing", href: "/contributing", sectionKey: "contributing" },
|
||||
]
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const mainNavItems: NavItem[] = [
|
||||
{ label: "Automate", href: "/automate" },
|
||||
{ label: "Deploy & Secure", href: "/deploy-secure" },
|
||||
{ label: "Kilo Gateway", href: "/gateway" },
|
||||
{ label: "KiloClaw", href: "/kiloclaw" },
|
||||
{ label: "Contributing", href: "/contributing" },
|
||||
]
|
||||
|
||||
|
||||
@@ -16,28 +16,6 @@ export const AutomateNav: NavSection[] = [
|
||||
],
|
||||
},
|
||||
{ href: "/automate/agent-manager", children: "Agent Manager" },
|
||||
{
|
||||
href: "/automate/kiloclaw/overview",
|
||||
children: "KiloClaw",
|
||||
subLinks: [
|
||||
{ href: "/automate/kiloclaw/overview", children: "Overview" },
|
||||
{ href: "/automate/kiloclaw/dashboard", children: "Dashboard" },
|
||||
{
|
||||
href: "/automate/kiloclaw/pre-installed-software",
|
||||
children: "Pre-installed Software",
|
||||
},
|
||||
{ href: "/automate/kiloclaw/control-ui", children: "Control UI" },
|
||||
{
|
||||
href: "/automate/kiloclaw/chat-platforms",
|
||||
children: "Chat Platforms",
|
||||
},
|
||||
{
|
||||
href: "/automate/kiloclaw/troubleshooting",
|
||||
children: "Troubleshooting",
|
||||
},
|
||||
{ href: "/automate/kiloclaw/pricing", children: "Pricing" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,10 +50,6 @@ export const ContributingNav: NavSection[] = [
|
||||
href: "/contributing/architecture/mcp-oauth-authorization",
|
||||
children: "MCP OAuth Authorization",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/model-provider-blocklist",
|
||||
children: "Model/Provider Blocklist",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/onboarding-improvements",
|
||||
children: "Onboarding Improvements",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CustomizeNav } from "./customize"
|
||||
import { DeploySecureNav } from "./deploy-secure"
|
||||
import { GatewayNav } from "./gateway"
|
||||
import { GettingStartedNav } from "./getting-started"
|
||||
import { KiloClawNav } from "./kiloclaw"
|
||||
import { ToolsNav } from "./tools"
|
||||
|
||||
export const Nav = {
|
||||
@@ -19,5 +20,6 @@ export const Nav = {
|
||||
ContributingNav,
|
||||
AiProvidersNav,
|
||||
GatewayNav,
|
||||
KiloClawNav,
|
||||
ToolsNav,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const KiloClawNav: NavSection[] = [
|
||||
{
|
||||
title: "KiloClaw",
|
||||
links: [
|
||||
{ href: "/kiloclaw/overview", children: "Overview" },
|
||||
{ href: "/kiloclaw/dashboard", children: "Dashboard" },
|
||||
{ href: "/kiloclaw/pre-installed-software", children: "Pre-installed Software" },
|
||||
{ href: "/kiloclaw/control-ui", children: "Control UI" },
|
||||
{ href: "/kiloclaw/chat-platforms", children: "Chat Platforms" },
|
||||
{ href: "/kiloclaw/version-pinning", children: "Version Pinning" },
|
||||
{ href: "/kiloclaw/troubleshooting", children: "Troubleshooting" },
|
||||
{ href: "/kiloclaw/pricing", children: "Pricing" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -13,6 +13,11 @@ module.exports = withMarkdoc(/* config: https://markdoc.io/docs/nextjs#options *
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/kiloclaw",
|
||||
destination: "/kiloclaw/overview",
|
||||
permanent: false,
|
||||
},
|
||||
...previousDocsRedirects,
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
title: "KiloClaw Pricing"
|
||||
description: "Pricing details for KiloClaw instances and model inference"
|
||||
---
|
||||
|
||||
# KiloClaw Pricing
|
||||
|
||||
KiloClaw uses your existing Kilo Gateway credits—there's no separate billing or subscription:
|
||||
|
||||
- **Instance hosting** — Free for 7 days during beta
|
||||
- **Model inference** — Charged against your Gateway credit balance
|
||||
- **Free models** — Several models are available at no cost. See the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models) for current availability.
|
||||
|
||||
See [Gateway Usage and Billing](/docs/gateway/usage-and-billing) for credit pricing details.
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms)
|
||||
- [Gateway Usage and Billing](/docs/gateway/usage-and-billing)
|
||||
@@ -90,7 +90,7 @@ You may want to select a specific model instead when:
|
||||
## Feedback
|
||||
|
||||
{% callout type="note" title="Help Us Improve" %}
|
||||
Auto Model is a new feature and we're actively improving it. We'd love to hear how it's working for you! Share feedback in our [Discord](https://discord.gg/kilocode) or [open an issue on GitHub](https://github.com/Kilo-Org/kilocode/issues).
|
||||
Auto Model is a new feature and we're actively improving it. We'd love to hear how it's working for you! Share feedback in our [Discord](https://kilo.ai/discord) or [open an issue on GitHub](https://github.com/Kilo-Org/kilocode/issues).
|
||||
{% /callout %}
|
||||
|
||||
## Related
|
||||
|
||||
@@ -5,44 +5,73 @@ description: "Control which AI models your team can access"
|
||||
|
||||
# Model Access Controls
|
||||
|
||||
**Model Access** lets organization admins control which AI models and providers are available to team members.
|
||||
Admins can **enable or disable** specific models, filter by attributes, and enforce organizational data policies.
|
||||
{% callout type="info" %}
|
||||
This is an **Enterprise-only** feature. Organizations on other plans have unrestricted access to all models and providers.
|
||||
{% /callout %}
|
||||
|
||||
**Model Access Controls** let organization owners block specific AI models or providers for all team members. The system uses a **blocklist** approach: everything is allowed by default, and admins explicitly block what should not be accessible.
|
||||
|
||||
This means newly added models and providers are automatically available to your team without any manual action required.
|
||||
|
||||
## How It Works
|
||||
|
||||
| Scenario | Behavior |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------- |
|
||||
| No blocks configured | All models and providers are available (default) |
|
||||
| Provider blocked | All current and future models from that provider are unavailable |
|
||||
| Specific model blocked | Only that model is unavailable; other models from the same provider remain accessible |
|
||||
|
||||
## Managing Model Access
|
||||
|
||||
1. Navigate to the **Model Access** tab of the Enterprise Dashboard.
|
||||
2. Toggle the checkbox beside any model or provider to enable or disable access.
|
||||
3. Click "Save Changes" to apply
|
||||
Navigate to your organization's **Providers & Models** page to configure access controls.
|
||||
|
||||
{% image width="800" alt="Model-Access-Select" src="https://github.com/user-attachments/assets/af71353d-facc-4d4b-a0cd-c7f2cea73e97" /%}
|
||||
The page has two tabs:
|
||||
|
||||
## Filtering Models
|
||||
### Models Tab
|
||||
|
||||
You can filter available models by:
|
||||
Lists all available models across all providers. For each model you can:
|
||||
|
||||
| Filter | Description |
|
||||
| ----------------------------- | --------------------------------------------------------------------------- |
|
||||
| **Data Policy** | Choose models that meet specific data retention or compliance requirements. |
|
||||
| **Provider Location** | Restrict models hosted in certain geographic regions. |
|
||||
| **Series** | Filter by model family (e.g. GPT-4, Claude 3, Gemini 1.5). |
|
||||
| **Provider** | Limit access to specific providers like OpenAI, Anthropic, or Google. |
|
||||
| **Input / Output Modalities** | Filter by capabilities (text, code, image, audio, etc.). |
|
||||
| **Pricing** | Compare cost per token or usage tier. |
|
||||
- Toggle access on or off
|
||||
- Search by model name, ID, or provider
|
||||
- Filter to show only currently allowed models
|
||||
|
||||
Select multiple filters for increased granularity.
|
||||
### Providers Tab
|
||||
|
||||
---
|
||||
Lists all providers. For each provider you can:
|
||||
|
||||
- Toggle the entire provider on or off (blocks all current and future models from that provider)
|
||||
- Filter by data policy (trains on data, retains prompts)
|
||||
- Filter by provider location / datacenter region
|
||||
|
||||
When you toggle a provider off, all models it offers become unavailable to team members. Re-enabling the provider restores access to all its models.
|
||||
|
||||
### Saving Changes
|
||||
|
||||
A status bar appears at the bottom of the page whenever you have unsaved changes. Click **Save** to apply your changes, or **Cancel** to discard them. Changes take effect immediately for all team members once saved.
|
||||
|
||||
## Filtering Options
|
||||
|
||||
Use filters to find the models or providers you want to block:
|
||||
|
||||
| Filter | Tab | Description |
|
||||
| ------------------- | ------------------ | ----------------------------------------------------- |
|
||||
| **Search** | Models & Providers | Filter by name, ID, or provider slug |
|
||||
| **Enabled only** | Models & Providers | Show only currently allowed items |
|
||||
| **Trains on data** | Providers | Filter by whether the provider trains on user prompts |
|
||||
| **Retains prompts** | Providers | Filter by whether the provider retains user prompts |
|
||||
| **Location** | Providers | Filter by provider headquarters or datacenter country |
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
- **Security-first teams**: Disable models that store prompts or operate outside your data region.
|
||||
- **Cost control**: Limit access to higher-priced models.
|
||||
- **Specialization**: Enable models that are optimized for specific tasks.
|
||||
- **Data compliance**: Block providers that train on prompts or operate outside your required data region.
|
||||
- **Cost control**: Block high-cost models to prevent accidental expensive usage.
|
||||
- **Security policy**: Restrict access to a known set of approved providers.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Only **Admins** and **Owners** can modify model access.
|
||||
- Updates propagate to all team members within seconds.
|
||||
- Only **Owners** can modify model access controls.
|
||||
- Individual users cannot override organization-level restrictions.
|
||||
- Blocking a provider blocks all its models, including models added by that provider in the future.
|
||||
- Unblocking a provider immediately restores access to all its models.
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: "Model/Provider Blocklist"
|
||||
description: "Proposal to replace the model/provider allowlist with a blocklist approach for enterprise team management"
|
||||
---
|
||||
|
||||
# Model/Provider Blocklist
|
||||
|
||||
## Overview
|
||||
|
||||
Enterprise organization administrators currently manage which models and providers their team members can use through an **allowlist** system in the Providers & Models settings page. This system stores two lists in organization settings: one for allowed models and one for allowed providers. It has proven confusing for customers and adds unnecessary friction.
|
||||
|
||||
- By default, an empty allowlist means "allow everything." Once an admin customizes any setting, new models added by providers are **not** automatically available -- the admin must manually approve each one.
|
||||
- An "Allow all current and future models" checkbox was added per-provider to address this. It works by adding a provider wildcard entry to the model allow list, which allows any model offered by that provider (including future ones). However, it has a critical flaw: if an admin disables one specific model that was allowed via the wildcard, the wildcard itself is removed. The admin is then forced back into manual per-model curation. Additionally, you have to set this manually for each provider.
|
||||
- The net result is that admins must either allow everything wholesale or commit to ongoing manual curation of hundreds of model/provider combinations.
|
||||
|
||||
This proposal replaces the allowlist with a **blocklist** approach. The default behavior becomes "everything is allowed unless explicitly blocked," which eliminates the ongoing maintenance burden while still giving admins precise control.
|
||||
|
||||
## Requirements
|
||||
|
||||
- This feature remains restricted to **enterprise plans only**, consistent with the current allowlist system. Teams-plan organizations get unrestricted model/provider access.
|
||||
- All models and providers are **allowed by default**, including newly added ones.
|
||||
- Admins can block an entire provider (all current and future models from that provider).
|
||||
- Admins can block a specific model/provider combination without affecting other providers offering the same model.
|
||||
- The UI must make it easy to find and block specific models across a large catalog (300+ models, 65+ providers).
|
||||
- Migration from the existing allowlist data must be handled without disrupting current customer configurations.
|
||||
|
||||
### Non-requirements
|
||||
|
||||
- Blocking a model across _all_ current and future providers (e.g., "block model X regardless of who offers it"). This can be added later if there is demand, but adds complexity and is not needed for the initial implementation.
|
||||
- Per-user or per-team blocklists. This proposal covers organization-level controls only.
|
||||
- Cost controls or spending limits per model. This is a separate concern.
|
||||
|
||||
## System Design
|
||||
|
||||
### Core Semantics
|
||||
|
||||
The system shifts from "deny by default, explicitly allow" to **"allow by default, explicitly deny"**:
|
||||
|
||||
| Scenario | Behavior |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| No blocklist entries | All models and providers are available (default) |
|
||||
| Provider blocked | All current and future models from that provider are unavailable. The same model may still be available from other providers. |
|
||||
| Specific model/provider combo blocked | Only that specific combination is unavailable. The model remains available from other providers, and other models from that provider remain available. |
|
||||
|
||||
### Plan Gating
|
||||
|
||||
Blocklist enforcement only applies to enterprise-plan organizations. For non-enterprise organizations (including teams plans), the blocklist fields are ignored and all models/providers are available. This mirrors the current allowlist behavior.
|
||||
|
||||
The mutation to update blocklists must remain gated behind organization owner permissions and an enterprise plan check, consistent with the existing allowlist mutation.
|
||||
|
||||
### Implementation design
|
||||
|
||||
TBD
|
||||
|
||||
### UI Design
|
||||
|
||||
Replace the current dual-tab (Models / Providers) layout with a **single unified view** organized by provider:
|
||||
|
||||
**Main view: Provider list with expandable models**
|
||||
|
||||
- A flat list of all providers, each expandable to show its offered models.
|
||||
- Each provider row has a block/unblock toggle. Blocking a provider visually marks all its models as blocked.
|
||||
- Each model row (within an expanded provider) has a block/unblock toggle for that specific model/provider combination.
|
||||
- A **free-text search/filter box** at the top filters both providers and models. For example, typing "K2.5" filters the provider list to only those offering a matching model, and within each provider only shows the matching models. This makes it easy to block a specific model across select providers. Providers are auto-expanded to show the matching models.
|
||||
- Blocked items are visually distinct (e.g., a red/muted treatment) so the current block state is immediately clear.
|
||||
- A summary indicator shows total blocked count (e.g., "3 providers blocked, 7 model combinations blocked").
|
||||
|
||||
**Interaction examples:**
|
||||
|
||||
| Action | Result |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| Block provider "Chutes" | All Chutes models become unavailable. Future models from Chutes are also blocked. |
|
||||
| Search "K2.5", block it under Fireworks | Only K2.5 via Fireworks is blocked. K2.5 via other providers is unaffected. |
|
||||
|
||||
## Features for the Future
|
||||
|
||||
- **Cross-provider model blocking**: Block a model ID across all current and future providers (e.g., "block anthropic/claude-opus-4.6 regardless of which provider serves it"). Deferred unless there is significant demand.
|
||||
- **Per-team / per-project blocklists**: Allow different teams within an organization to have different blocklist policies.
|
||||
- **Cost-based controls**: Automatically block models above a certain price threshold.
|
||||
- **Temporary blocks**: Time-limited blocks for models under evaluation or during incident response.
|
||||
@@ -19,6 +19,7 @@ A user or organization may want to use BYOK to:
|
||||
Kilo Gateway currently supports BYOK keys for these providers:
|
||||
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- OpenAI
|
||||
- Google AI Studio
|
||||
- Minimax
|
||||
@@ -33,6 +34,29 @@ Kilo Gateway currently supports BYOK keys for these providers:
|
||||
3. Click `Add Your First Key`, select the provider, and paste your API key.
|
||||
4. Save.
|
||||
|
||||
### AWS Bedrock configuration
|
||||
|
||||
AWS Bedrock requires credentials in a different format than other providers. Instead of a single API key, you must provide your AWS credentials as a JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "AKIA...",
|
||||
"secretAccessKey": "...",
|
||||
"region": "us-east-1"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------ |
|
||||
| `accessKeyId` | Your AWS access key ID |
|
||||
| `secretAccessKey` | Your AWS secret access key |
|
||||
| `region` | The AWS region where Bedrock is enabled (e.g., `us-east-1`, `eu-west-1`) |
|
||||
|
||||
Your IAM user or role must have the following permissions:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
|
||||
## 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.
|
||||
|
||||
+48
-10
@@ -5,27 +5,65 @@ description: "Connect your KiloClaw agent to Telegram, Discord, Slack, and more"
|
||||
|
||||
# Connecting Chat Platforms
|
||||
|
||||
KiloClaw supports connecting your AI agent to Telegram, Discord, and Slack. You can configure channels from the **Settings** tab on your [KiloClaw dashboard](/docs/automate/kiloclaw/dashboard#channels), or from the OpenClaw Control UI after accessing your instance.
|
||||
KiloClaw supports connecting your AI agent to Telegram, Discord, and Slack. You can configure channels from the **Settings** tab on your [KiloClaw dashboard](/docs/kiloclaw/dashboard#channels), or from the OpenClaw Control UI after accessing your instance.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### Telegram
|
||||
|
||||
To connect Telegram, you need a **Bot Token** from [@BotFather](https://t.me/BotFather) on Telegram.
|
||||
|
||||
Enter the token in the Settings tab and click **Save**. You can remove or replace a configured token at any time.
|
||||
1. Open Telegram and search for [@BotFather](https://t.me/BotFather)
|
||||
2. Send `/newbot` and follow the prompts to create your bot
|
||||
3. Copy the **Bot Token** that BotFather gives you
|
||||
4. Go to the **Settings** tab on your [KiloClaw dashboard](/docs/kiloclaw/dashboard)
|
||||
5. Paste the token into the **Telegram Bot Token** field
|
||||
6. Click **Save**
|
||||
7. Redeploy your KiloClaw instance
|
||||
|
||||
{% image src="/docs/img/kiloclaw/telegram.png" alt="Connect account screen" width="800" caption="Telegram bot token entry" /%}
|
||||
|
||||
Advanced settings such as DM policy, allow lists, and groups can be configured in the OpenClaw Control UI after connecting.
|
||||
You can remove or replace a configured token at any time.
|
||||
|
||||
> ℹ️ **Info**
|
||||
> Advanced settings such as DM policy, allow lists, and groups can be configured in the OpenClaw Control UI after connecting.
|
||||
|
||||
### Discord
|
||||
|
||||
To connect Discord, you need a **Bot Token** from the [Discord Developer Portal](https://discord.com/developers/applications).
|
||||
#### Enable Privileged Intents
|
||||
|
||||
Still on the **Bot** page, scroll down to **Privileged Gateway Intents** and enable:
|
||||
|
||||
- **Message Content Intent** (required)
|
||||
- **Server Members Intent** (recommended — needed for role allowlists and name matching)
|
||||
- **Presence Intent** (optional)
|
||||
#### Copy Your Bot Token
|
||||
|
||||
1. Scroll back up on the **Bot** page and click **Reset Token**
|
||||
|
||||
> 📝 **Note**
|
||||
> Despite the name, this generates your first token — nothing is being "reset."
|
||||
|
||||
2. Copy the token that appears and paste it into the **Discord Bot Token** field in your KiloClaw dashboard.
|
||||
|
||||
{% image src="/docs/img/kiloclaw/discord.png" alt="Connect account screen" width="800" caption="Discord bot token entry" /%}
|
||||
|
||||
Enter the token in the Settings tab and click **Save**. You can remove or replace a configured token at any time.
|
||||
#### Generate an Invite URL and Add the Bot to Your Server
|
||||
|
||||
1. Click **OAuth2** on the sidebar
|
||||
2. Scroll down to **OAuth2 URL Generator** and enable:
|
||||
- `bot`
|
||||
- `applications.commands`
|
||||
3. A **Bot Permissions** section will appear below. Enable:
|
||||
- View Channels
|
||||
- Send Messages
|
||||
- Read Message History
|
||||
- Embed Links
|
||||
- Attach Files
|
||||
- Add Reactions (optional)
|
||||
4. Copy the generated URL at the bottom
|
||||
5. Paste it into your browser, select your server, and click **Continue**
|
||||
6. You should now see your bot in the Discord server
|
||||
|
||||
### Slack
|
||||
|
||||
@@ -40,7 +78,7 @@ Both tokens are required — you cannot save with only one.
|
||||
|
||||
## Configuring a Channel
|
||||
|
||||
1. Open your [KiloClaw dashboard](/docs/automate/kiloclaw/dashboard)
|
||||
1. Open your [KiloClaw dashboard](/docs/kiloclaw/dashboard)
|
||||
2. Go to the **Settings** tab
|
||||
3. Scroll to the **Channels** section
|
||||
4. Enter the required token(s) for your platform
|
||||
@@ -69,8 +107,8 @@ Additional platforms (such as WhatsApp) are planned for future releases. For the
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/automate/kiloclaw/dashboard)
|
||||
- [Troubleshooting](/docs/automate/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/automate/kiloclaw/pricing)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [Troubleshooting](/docs/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/kiloclaw/pricing)
|
||||
- [OpenClaw Documentation](https://docs.openclaw.ai)
|
||||
+24
-10
@@ -5,7 +5,7 @@ description: "Browser-based dashboard for managing your OpenClaw instance"
|
||||
|
||||
# OpenClaw Control UI
|
||||
|
||||
The Control UI is a browser-based dashboard (built with Vite + Lit) served by the OpenClaw Gateway on the same port as the gateway itself (default: `http://localhost:18789/`). It connects via WebSocket and gives you real-time control over your agent, channels, sessions, and system configuration. For KiloClaw users, see [Accessing the Control UI](/docs/automate/kiloclaw/dashboard#accessing-the-control-ui) to get started.
|
||||
The Control UI is a browser-based dashboard (built with Vite + Lit) served by the OpenClaw Gateway on the same port as the gateway itself (default: `http://localhost:18789/`). It connects via WebSocket and gives you real-time control over your agent, channels, sessions, and system configuration. For KiloClaw users, see [Accessing the Control UI](/docs/kiloclaw/dashboard#accessing-the-control-ui) to get started.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -24,12 +24,30 @@ The Control UI is a browser-based dashboard (built with Vite + Lit) served by th
|
||||
For more details, please see the official [OpenClaw documentation](https://docs.openclaw.ai/web/control-ui).
|
||||
|
||||
{% callout type="warning" %}
|
||||
Do not use the **Update** feature in the Control UI to update KiloClaw. Use **Redeploy** from the [KiloClaw Dashboard](/docs/automate/kiloclaw/dashboard#redeploy) instead. Updating via the Control UI will not apply the correct KiloClaw platform image and may break your instance.
|
||||
Do not use the **Update** feature in the Control UI to update KiloClaw. Use **Redeploy** from the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#redeploy) instead. Updating via the Control UI will not apply the correct KiloClaw platform image and may break your instance.
|
||||
{% /callout %}
|
||||
|
||||
## Changing Models
|
||||
|
||||
The Control UI Chat tab doubles as a command line for model management. KiloClaw exposes 335+ models through the `kilocode` provider and you can browse and switch between them without leaving the chat.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `/model status` | View the currently active model and provider |
|
||||
| `/models kilocode` | Browse available models (paginated, 20 per page) |
|
||||
| `/models kilocode <page>` | Jump to a specific page (e.g. `/models kilocode 2`) |
|
||||
| `/model kilocode/<provider>/<model>` | Switch to a specific model (e.g. `/model kilocode/anthropic/claude-sonnet-4.6`) |
|
||||
| `/models kilocode all` | List every available model at once |
|
||||
|
||||
Each `/models` response includes helper text at the bottom with shortcuts for switching, paging, and listing all models.
|
||||
|
||||
To change the default model for all new sessions, edit `agents.defaults.model.primary` in your `openclaw.json` via **Config** in the Control UI (or the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#changing-the-model) for a quick dropdown pick).
|
||||
|
||||
For the full list of providers, advanced configuration, and CLI commands, see the [OpenClaw Model Providers documentation](https://docs.openclaw.ai/providers).
|
||||
|
||||
## Authentication
|
||||
|
||||
Auth is handled via token or password on the WebSocket handshake. We use the one time "access code" from your KiloClaw Dashboard to pair your device. Other remote connections require one-time device pairing — the pairing request appears on the [KiloClaw Dashboard](/docs/automate/kiloclaw/dashboard#pairing-requests) or in the Control UI itself.
|
||||
Auth is handled via token or password on the WebSocket handshake. Remote connections require one-time device pairing — the pairing request appears on the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#pairing-requests) or in the Control UI itself.
|
||||
|
||||
## Exec Approvals
|
||||
|
||||
@@ -91,12 +109,8 @@ Approval prompts can also be forwarded to chat channels (Slack, Telegram, Discor
|
||||
|
||||
Navigate to **Nodes > Exec Approvals** in the Control UI to edit defaults, per-agent overrides, and allowlists. Select a scope (Defaults or a specific agent), adjust the policy, add or remove allowlist patterns, then save.
|
||||
|
||||
{% callout type="info" %}
|
||||
If a node does not yet advertise exec approval capabilities, edit its `~/.openclaw/exec-approvals.json` file directly. You can also use the CLI: `openclaw approvals`.
|
||||
{% /callout %}
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Dashboard](/docs/automate/kiloclaw/dashboard)
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms)
|
||||
- [KiloClaw Dashboard](/docs/kiloclaw/dashboard)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms)
|
||||
+41
-13
@@ -5,7 +5,7 @@ description: "Managing your KiloClaw instance from the dashboard"
|
||||
|
||||
# KiloClaw Dashboard
|
||||
|
||||
This page covers everything you can do from the KiloClaw dashboard. For getting started, see [KiloClaw Overview](/docs/automate/kiloclaw/overview).
|
||||
This page covers everything you can do from the KiloClaw dashboard. For getting started, see [KiloClaw Overview](/docs/kiloclaw/overview).
|
||||
|
||||
{% image src="/docs/img/kiloclaw/dashboard.png" alt="Connect account screen" width="800" caption="The KiloClaw Dashboard" /%}
|
||||
|
||||
@@ -80,14 +80,44 @@ Gateway process info is only available when the machine is running.
|
||||
|
||||
Select a model from the dropdown and click **Save & Provision**. The API key is platform-managed and refreshes automatically when you save — you never need to enter one. The key has a 30-day expiry.
|
||||
|
||||
For access to the full catalog of 335+ models, use the `/model` and `/models` commands in the [Control UI Chat](/docs/kiloclaw/control-ui#changing-models).
|
||||
|
||||
### Channels
|
||||
|
||||
You can connect Telegram, Discord, and Slack by entering bot tokens in the Settings tab. See [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms) for setup instructions.
|
||||
You can connect Telegram, Discord, and Slack by entering bot tokens in the Settings tab. See [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms) for setup instructions.
|
||||
|
||||
{% callout type="info" %}
|
||||
After saving channel tokens, you need to **Redeploy** or **Restart OpenClaw** for the changes to take effect.
|
||||
{% /callout %}
|
||||
|
||||
### Version Pinning
|
||||
|
||||
You can pin your instance to a specific OpenClaw version and variant from the Settings tab. This gives you control over when you upgrade — your instance stays on the pinned version until you choose to change it.
|
||||
|
||||
Select a version and variant from the dropdowns and click **Save**. To return to automatic updates, clear the version pin and save.
|
||||
|
||||
See [Version Pinning](/docs/kiloclaw/version-pinning) for details.
|
||||
|
||||
### Restore Default Config
|
||||
|
||||
If your OpenClaw configuration gets corrupted — for example, if the agent edits `openclaw.json` and introduces an error — you can restore it without a full redeploy.
|
||||
|
||||
In **Settings > Danger Zone**, click **Restore Config**. This will:
|
||||
|
||||
1. Back up your current `openclaw.json` to `/root/.openclaw/`
|
||||
2. Rewrite `openclaw.json` from your environment variables (channel tokens, model settings, etc.)
|
||||
3. Restart the gateway
|
||||
|
||||
Your files, workspace, and persistent data are not affected. Only the OpenClaw configuration file is reset.
|
||||
|
||||
> 💡 **Tip**
|
||||
> If your instance is in a crash loop and you can't access the Control UI, try **Restore Config** from the KiloClaw dashboard first before redeploying.
|
||||
|
||||
{% callout type="warning" %}
|
||||
This action cannot be undone. Make sure you've saved any important changes to your configuration before restoring.
|
||||
{% /callout %}
|
||||
|
||||
|
||||
### Stop, Destroy & Restore
|
||||
|
||||
At the bottom of Settings:
|
||||
@@ -98,13 +128,11 @@ At the bottom of Settings:
|
||||
|
||||
## Accessing the Control UI
|
||||
|
||||
When your instance is running you can access the [OpenClaw Control UI](/docs/automate/kiloclaw/control-ui) — a browser-based dashboard for managing your agent, channels, sessions, exec approvals, and more:
|
||||
When your instance is running you can access the [OpenClaw Control UI](/docs/kiloclaw/control-ui) — a browser-based dashboard for managing your agent, channels, sessions, exec approvals, and more:
|
||||
|
||||
1. Click **Access Code** to generate a one-time code (expires in 10 minutes)
|
||||
2. Click **Open** to launch the OpenClaw web interface in a new tab
|
||||
3. Enter the access code to authenticate
|
||||
1. Click **Open** to launch the OpenClaw web interface in a new tab
|
||||
|
||||
See the [Control UI reference](/docs/automate/kiloclaw/control-ui) for a full overview of its capabilities.
|
||||
See the [Control UI reference](/docs/kiloclaw/control-ui) for a full overview of its capabilities.
|
||||
|
||||
{% callout type="warning" %}
|
||||
Do not use the **Update** feature in the OpenClaw Control UI to update KiloClaw. Use **Redeploy** from the KiloClaw Dashboard instead. Updating via the Control UI will not apply the correct KiloClaw platform image and may break your instance.
|
||||
@@ -117,7 +145,7 @@ When your instance is running, the dashboard shows any pending pairing requests.
|
||||
- Someone messages your bot on Telegram, Discord, or Slack for the first time
|
||||
- A new browser or device connects to the Control UI
|
||||
|
||||
You need to **approve** each request before the user or device can interact with your agent. See [Pairing Requests](/docs/automate/kiloclaw/chat-platforms#pairing-requests) for details.
|
||||
You need to **approve** each request before the user or device can interact with your agent. See [Pairing Requests](/docs/kiloclaw/chat-platforms#pairing-requests) for details.
|
||||
|
||||
## Changelog
|
||||
|
||||
@@ -155,8 +183,8 @@ These are the beta specifications for machines and subject to change without not
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [OpenClaw Control UI](/docs/automate/kiloclaw/control-ui)
|
||||
- [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms)
|
||||
- [Troubleshooting](/docs/automate/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/automate/kiloclaw/pricing)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [OpenClaw Control UI](/docs/kiloclaw/control-ui)
|
||||
- [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms)
|
||||
- [Troubleshooting](/docs/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/kiloclaw/pricing)
|
||||
+22
-45
@@ -5,7 +5,7 @@ description: "One-click deployment of your personal AI agent with OpenClaw"
|
||||
|
||||
# KiloClaw 🦀
|
||||
|
||||
KiloClaw is Kilo's hosted [OpenClaw](https://openclaw.ai) service — a one-click deployment that gives you a personal AI agent without the complexity of self-hosting. OpenClaw is an open source AI agent that connects to chat platforms like Telegram, Discord, and Slack.
|
||||
KiloClaw is Kilo's hosted [OpenClaw](https://openclaw.ai) service — a one-click deployment that gives you a personal AI agent without the complexity of self-hosting. OpenClaw is a 24/7, open source AI agent that connects to chat platforms like Telegram, Discord, and Slack so it can take real actions automatically, not just chat.
|
||||
|
||||
KiloClaw is powered by KiloCode. The API key is platform-managed, so you never need to bring your own. KiloClaw is currently in **Beta**.
|
||||
|
||||
@@ -21,7 +21,11 @@ KiloClaw is powered by KiloCode. The API key is platform-managed, so you never n
|
||||
## Prerequisites
|
||||
|
||||
- **Kilo account** — Sign up at [kilo.ai](https://kilo.ai) if you haven't already
|
||||
- **Gateway credits** — KiloClaw uses your existing [Gateway credits](/docs/gateway/usage-and-billing) for model inference
|
||||
- **Model access** — KiloClaw uses **Kilo Gateway by default**, which provides access to **500+ AI models** through a single integration.
|
||||
|
||||
You can also run KiloClaw using:
|
||||
|
||||
- **Your own provider API keys (BYOK)** such as Anthropic, OpenAI, Google, or other supported providers.
|
||||
|
||||
## Creating an Instance
|
||||
|
||||
@@ -35,7 +39,7 @@ KiloClaw is powered by KiloCode. The API key is platform-managed, so you never n
|
||||
|
||||
{% image src="/docs/img/kiloclaw/create-instance.png" alt="Create instance modal with model selection" width="600" caption="Model selection during instance creation" /%}
|
||||
|
||||
5. Optionally configure chat channels (Telegram, Discord, Slack) — you can also do this later from [Settings](/docs/automate/kiloclaw/dashboard#settings)
|
||||
5. Optionally configure chat channels (Telegram, Discord, Slack) — you can also do this later from [Settings](/docs/kiloclaw/dashboard#settings)
|
||||
6. Click **Create & Provision**
|
||||
|
||||
Your instance will be provisioned in seconds. Each instance runs on a dedicated machine with 2 shared vCPUs, 3 GB RAM, and a 10 GB persistent SSD. Once created in a region, your instance always runs there.
|
||||
@@ -53,7 +57,7 @@ The KiloClaw dashboard gives you full control over your instance.
|
||||
- **Redeploy** — This will stop the machine, apply any pending image or config updates, and restart it. The machine will be briefly offline.
|
||||
- **OpenClaw Doctor** — Run diagnostics and auto-fix common issues
|
||||
|
||||
For full details on each control and when to use them, see the [Dashboard Reference](/docs/automate/kiloclaw/dashboard).
|
||||
For full details on each control and when to use them, see the [Dashboard Reference](/docs/kiloclaw/dashboard).
|
||||
|
||||
### Changelog
|
||||
|
||||
@@ -61,16 +65,11 @@ The dashboard shows recent platform updates. Some updates include a deploy hint
|
||||
|
||||
### Pairing Requests
|
||||
|
||||
When you initialize a new channel for the first time, or a new device connects to the Control UI, you'll see a pairing request on the dashboard that you need to approve. See [Pairing Requests](/docs/automate/kiloclaw/chat-platforms#pairing-requests) for details.
|
||||
When you initialize a new channel for the first time, or a new device connects to the Control UI, you'll see a pairing request on the dashboard that you need to approve. See [Pairing Requests](/docs/kiloclaw/chat-platforms#pairing-requests) for details.
|
||||
|
||||
## Accessing Your Agent
|
||||
|
||||
1. Click **Access Code** to get a one-time code (expires in 10 minutes)
|
||||
|
||||
{% image src="/docs/img/kiloclaw/access-code-modal.png" alt="Access code modal showing one-time code" width="500" caption="One-time access code with 10-minute expiration" /%}
|
||||
|
||||
2. Click **Open** to launch the OpenClaw web interface
|
||||
3. Enter your access code to authenticate
|
||||
1. Click **Open** on your dashboard to launch the OpenClaw web interface
|
||||
|
||||
{% image src="/docs/img/kiloclaw/openclaw-dashboard.png" alt="OpenClaw web interface" width="800" caption="OpenClaw web UI" /%}
|
||||
|
||||
@@ -78,49 +77,27 @@ When you initialize a new channel for the first time, or a new device connects t
|
||||
|
||||
OpenClaw lets you customize your own AI assistant that can actually take action — check your email, manage your calendar, control smart devices, browse the web, and message you on Telegram or Discord when something needs attention. It's like having a personal assistant that runs 24/7, with the skills and access you choose to give it.
|
||||
|
||||
### Browser Tool
|
||||
|
||||
KiloClaw includes a headless Chromium browser, enabling your agent to browse the web, take screenshots, and automate web interactions using the OpenClaw browser tool. This works out of the box with the "full" tool profile — no additional setup needed.
|
||||
|
||||
### Default Tool Profile
|
||||
|
||||
New KiloClaw instances deploy with the **full** tool profile by default, giving your agent unrestricted access to all available tools — filesystem operations, shell execution, web search, browser automation, messaging, memory, sub-agents, and more.
|
||||
|
||||
For more information on use cases:
|
||||
|
||||
- [OpenClaw Showcase](https://docs.openclaw.ai/start/showcase)
|
||||
- [100 hours of OpenClaw in 35 Minutes](https://www.youtube.com/watch?v=_kZCoW-Qxnc)
|
||||
- [Clawhub](https://clawhub.ai/): search for skills
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
KiloClaw deploys with the **full** tool profile, giving your agent unrestricted access to all available tools — filesystem operations (read, write, edit), shell execution, web search, messaging, memory, sub-agents, and more.
|
||||
|
||||
Shell commands are still gated by an **allowlist** — unknown commands trigger an approval prompt in the Control UI.
|
||||
|
||||
### Changing the Tool Profile
|
||||
|
||||
If you want to restrict your agent's capabilities, you can change the tool profile from the OpenClaw Control UI:
|
||||
|
||||
1. Open your agent's Control UI (via **OpenClaw** on the dashboard)
|
||||
2. Navigate to **Settings** → **Config** → **Tools** → **Tool Profile**
|
||||
3. Select a different profile.
|
||||
|
||||
Your choice persists across restarts — KiloClaw won't overwrite a customized profile.
|
||||
|
||||
For a complete and up-to-date tool configuration reference, see the [OpenClaw Tools documentation](https://docs.openclaw.ai/tools#tools-openclaw).
|
||||
|
||||
## Limitations
|
||||
|
||||
KiloClaw is currently in **beta**. Current constraints include:
|
||||
|
||||
- **One instance per account** — Each user can run a single KiloClaw instance
|
||||
- **Model availability** — Some models may have rate limits during high demand
|
||||
- **Session persistence** — Chat history may be cleared during beta updates
|
||||
- **Feature parity** — Not all OpenClaw features are available in the hosted version yet
|
||||
|
||||
{% callout type="info" %}
|
||||
Have feedback or running into issues? Join the [Kilo Discord](https://kilo.ai/discord) and share it in the KiloClaw channel.
|
||||
{% /callout %}
|
||||
|
||||
## Related
|
||||
|
||||
- [Dashboard Reference](/docs/automate/kiloclaw/dashboard)
|
||||
- [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms)
|
||||
- [Troubleshooting](/docs/automate/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/automate/kiloclaw/pricing)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms)
|
||||
- [Troubleshooting](/docs/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Pricing](/docs/kiloclaw/pricing)
|
||||
- [Gateway Usage and Billing](/docs/gateway/usage-and-billing)
|
||||
- [Agent Manager](/docs/automate/agent-manager)
|
||||
- [OpenClaw Documentation](https://docs.openclaw.ai)
|
||||
+32
-14
@@ -39,6 +39,13 @@ The following packages are installed via `apt` on top of the base image:
|
||||
| `ffmpeg` | Audio/video processing |
|
||||
| `tmux` | Terminal multiplexer |
|
||||
|
||||
## Browser
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| Headless Chromium | Built-in browser for web browsing, screenshots, and CDP automation. Works with OpenClaw's browser tool out of the box. Requires the "full" tool profile. |
|
||||
|
||||
|
||||
## Languages & Runtimes
|
||||
|
||||
| Language / Runtime | Version | Install Method |
|
||||
@@ -65,6 +72,7 @@ These package managers are available for installing libraries and dependencies:
|
||||
| GitHub CLI (`gh`) | Unpinned (GitHub apt repo) |
|
||||
| 1Password CLI (`op`) | 2.32.1 (1Password apt repo) |
|
||||
|
||||
|
||||
## npm Global Packages
|
||||
|
||||
The following packages are installed globally via `npm`:
|
||||
@@ -75,25 +83,35 @@ The following packages are installed globally via `npm`:
|
||||
| mcporter | 0.7.3 |
|
||||
| `@steipete/summarize` | 0.11.1 |
|
||||
|
||||
## Go Tools
|
||||
## OpenClaw Skills & Integrations
|
||||
|
||||
These tools are pre-installed via `go install` and available on `$PATH`:
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| gog (gogcli) | Google Workspace CLI — Gmail, Calendar, Drive, Contacts, Sheets, Docs |
|
||||
| blogwatcher | Monitor blogs and RSS/Atom feeds for updates |
|
||||
| xurl | Authenticated requests to the X (Twitter) API |
|
||||
| gifgrep | Search GIF providers, download results, extract stills |
|
||||
| summarize | Summarize or extract text/transcripts from URLs and files |
|
||||
| goplaces | Location and places lookup |
|
||||
|
||||
| Tool | Version |
|
||||
| ------------- | ------- |
|
||||
| `gog` | 0.11.0 |
|
||||
| `goplaces` | 0.3.0 |
|
||||
| `blogwatcher` | 0.0.2 |
|
||||
| `xurl` | 1.0.3 |
|
||||
| `gifgrep` | 0.2.3 |
|
||||
|
||||
## Installing Additional Tools
|
||||
|
||||
Your agent can install additional tools at runtime:
|
||||
|
||||
- **Go packages:** `go install github.com/example/tool@latest`
|
||||
- **Node packages:** `npm install -g <package>`
|
||||
- **Python packages:** `pip install <package>`
|
||||
|
||||
{% callout type="tip" %}
|
||||
These tools receive updates when you **Upgrade & Redeploy** your instance from the [KiloClaw Dashboard](/docs/automate/kiloclaw/dashboard#redeploy). Check the changelog for image update announcements.
|
||||
These tools receive updates when you **Upgrade & Redeploy** your instance from the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#redeploy). Check the changelog for image update announcements.
|
||||
{% /callout %}
|
||||
|
||||
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/automate/kiloclaw/dashboard)
|
||||
- [Machine Specs](/docs/automate/kiloclaw/dashboard#machine-specs)
|
||||
- [Troubleshooting](/docs/automate/kiloclaw/troubleshooting)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [Machine Specs](/docs/kiloclaw/dashboard#machine-specs)
|
||||
- [Troubleshooting](/docs/kiloclaw/troubleshooting)
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "KiloClaw Pricing"
|
||||
description: "Pricing details for KiloClaw instances and model inference"
|
||||
---
|
||||
|
||||
# KiloClaw Pricing
|
||||
|
||||
KiloClaw uses Kilo Gateway credits by default — if you route requests through BYOK, model usage is billed directly by your provider instead.
|
||||
|
||||
### Instance Hosting
|
||||
|
||||
KiloClaw hosting is **free during the beta period**. Each user gets a dedicated machine (2 shared vCPUs, 3 GB RAM, 10 GB SSD) at no cost.
|
||||
|
||||
> ℹ️ **Info**
|
||||
> Beta pricing is subject to change. Paid hosting tiers may be introduced after the beta period ends. Any changes will be announced in advance.
|
||||
|
||||
### Model Inference
|
||||
|
||||
Model usage is charged against your [Gateway credit balance](/docs/gateway/usage-and-billing). Costs vary by model — premium models like Claude Opus or GPT-5.4-pro cost more per token than smaller models.
|
||||
|
||||
### Free Models
|
||||
|
||||
Several models are available at **no additional cost** to your Gateway balance. These are great for getting started or for tasks that don't need the most powerful models.
|
||||
|
||||
To see which models are currently free, check the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models) — free models are marked accordingly.
|
||||
|
||||
### Adding Credits
|
||||
|
||||
You can add Gateway credits from your [Kilo account](https://app.kilo.ai). Credits are shared across all Kilo products (VSCode extension, CLI, Cloud Agents, and KiloClaw).
|
||||
|
||||
See [Adding Credits](/docs/getting-started/adding-credits) and [Gateway Usage and Billing](/docs/gateway/usage-and-billing) for details.
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms)
|
||||
- [Gateway Usage and Billing](/docs/gateway/usage-and-billing)
|
||||
+26
-11
@@ -12,7 +12,7 @@ OpenClaw Doctor is the recommended first step when something isn't working. It r
|
||||
To use it:
|
||||
|
||||
1. Make sure your instance is running
|
||||
2. Click **OpenClaw Doctor** on your [dashboard](/docs/automate/kiloclaw/dashboard)
|
||||
2. Click **OpenClaw Doctor** on your [dashboard](/docs/kiloclaw/dashboard)
|
||||
3. Watch the output as it runs — results appear in real time
|
||||
|
||||
## Common Questions
|
||||
@@ -28,9 +28,9 @@ No. Redeploy does **not** delete your files, git repos, or cron jobs. It stops t
|
||||
|
||||
### My bot isn't responding on Telegram/Discord/Slack
|
||||
|
||||
1. Check that the channel token is configured in [Settings](/docs/automate/kiloclaw/dashboard#channels)
|
||||
1. Check that the channel token is configured in [Settings](/docs/kiloclaw/dashboard#channels)
|
||||
2. Make sure you **Redeployed** or **Restarted OpenClaw** after saving tokens
|
||||
3. Check for pending [pairing requests](/docs/automate/kiloclaw/chat-platforms#pairing-requests) — the user may need to be approved
|
||||
3. Check for pending [pairing requests](/docs/kiloclaw/chat-platforms#pairing-requests) — the user may need to be approved
|
||||
4. Try running **OpenClaw Doctor**
|
||||
|
||||
### The gateway shows "Crashed"
|
||||
@@ -41,10 +41,6 @@ The OpenClaw process is automatically restarted when it crashes. Check the Gatew
|
||||
2. Try a **Redeploy** to apply the latest platform image
|
||||
3. If the issue persists, join the [Kilo Discord](https://kilo.ai/discord) and share details in the KiloClaw channel
|
||||
|
||||
### My access code isn't working
|
||||
|
||||
Access codes are one-time use and expire after 10 minutes. Generate a new one by clicking **Access Code** on the dashboard. Make sure your instance is running before clicking **Open**.
|
||||
|
||||
### I changed the model but the agent is still using the old one
|
||||
|
||||
After selecting a new model, click **Save & Provision** to apply it. This refreshes the API key and saves the new model. You may also need to **Restart OpenClaw** for the change to take full effect.
|
||||
@@ -60,6 +56,25 @@ The Gateway Process tab shows the current state of the OpenClaw process inside y
|
||||
- **Crashed** — The process exited unexpectedly and will be automatically restarted
|
||||
- **Shutting Down** — The process is stopping as part of a machine stop or redeploy
|
||||
|
||||
## FAQ
|
||||
|
||||
### How can I change my model?
|
||||
|
||||
You can change the model in two ways:
|
||||
|
||||
- **From chat** — Type `/model` in the Chat window within the OpenClaw Control UI to switch models directly.
|
||||
- **From the dashboard** — Go to [https://app.kilo.ai/claw](https://app.kilo.ai/claw), select the model you want, and click **Save**. No redeploy is needed.
|
||||
|
||||
### Can I access the filesystem?
|
||||
|
||||
Direct filesystem access is not available at this time. You can interact with files through your OpenClaw agent using its built-in file tools.
|
||||
|
||||
### How can I update my OpenClaw?
|
||||
|
||||
Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup.
|
||||
|
||||
Updates are managed by the KiloClaw platform team to ensure stability. When a new version is available, it will be announced in the **Changelog** on your dashboard. To apply the update, click **Upgrade & Redeploy** from the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#redeploy).
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
For advanced users — how KiloClaw instances are structured:
|
||||
@@ -72,7 +87,7 @@ For advanced users — how KiloClaw instances are structured:
|
||||
|
||||
## Related
|
||||
|
||||
- [KiloClaw Overview](/docs/automate/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/automate/kiloclaw/dashboard)
|
||||
- [Connecting Chat Platforms](/docs/automate/kiloclaw/chat-platforms)
|
||||
- [KiloClaw Pricing](/docs/automate/kiloclaw/pricing)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [Connecting Chat Platforms](/docs/kiloclaw/chat-platforms)
|
||||
- [KiloClaw Pricing](/docs/kiloclaw/pricing)
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "Version Pinning"
|
||||
description: "Pin your KiloClaw instance to a specific OpenClaw version and variant"
|
||||
---
|
||||
|
||||
# Version Pinning
|
||||
|
||||
Version pinning lets you lock your KiloClaw instance to a specific OpenClaw version and variant. This gives you control over when your instance upgrades — it stays on the pinned version until you explicitly change it.
|
||||
|
||||
## When to Use Version Pinning
|
||||
|
||||
Version pinning is useful when:
|
||||
|
||||
- A changelog entry is marked **Redeploy Required** and you're not ready to upgrade yet
|
||||
- You're running a workflow that depends on specific OpenClaw behavior
|
||||
- You want to test the impact of an upgrade before committing to it
|
||||
|
||||
## How to Pin a Version
|
||||
|
||||
1. Go to your [KiloClaw dashboard](https://app.kilo.ai/profile)
|
||||
2. Open the **Settings** tab
|
||||
3. Scroll to the **Version Pinning** section
|
||||
4. Select a **version** and **variant** from the dropdowns
|
||||
5. Click **Save**
|
||||
|
||||
Your instance will stay on the selected version until you change or clear the pin.
|
||||
|
||||
{% callout type="info" %}
|
||||
After saving a version pin, you need to **Redeploy** for the change to take effect on your running instance.
|
||||
{% /callout %}
|
||||
|
||||
## Variants
|
||||
|
||||
Each OpenClaw version is available in one or more variants. Variants may differ in included tools, default configuration, or base image. Select the variant that matches your use case, or use the default if unsure.
|
||||
|
||||
## Clearing a Pin
|
||||
|
||||
To return to automatic updates:
|
||||
|
||||
1. Go to **Settings > Version Pinning**
|
||||
2. Clear the version selection
|
||||
3. Click **Save**
|
||||
4. Use **Upgrade & Redeploy** from the dashboard to apply the latest platform version
|
||||
|
||||
{% callout type="warning" %}
|
||||
Clearing a pin and running **Upgrade & Redeploy** will update your instance to the latest supported platform version. Review the changelog before upgrading to check for breaking changes.
|
||||
{% /callout %}
|
||||
|
||||
## Related
|
||||
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Troubleshooting](/docs/kiloclaw/troubleshooting)
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = [
|
||||
{
|
||||
source: "/docs/automate/kiloclaw",
|
||||
destination: "/docs/automate/kiloclaw/overview",
|
||||
source: "/docs/contributing/architecture/model-provider-blocklist",
|
||||
destination: "/docs/collaborate/enterprise/model-access-controls",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
@@ -795,4 +795,10 @@ module.exports = [
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/docs/automate/kiloclaw/:path*",
|
||||
destination: "/docs/kiloclaw/:path*",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 73 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
@@ -116,6 +116,7 @@
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.5",
|
||||
"lucide-solid": "0.576.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// kilocode_change - new file
|
||||
import { createEffect, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
|
||||
const DEBOUNCE_MS = 100
|
||||
|
||||
export interface AutoScrollOptions {
|
||||
working: () => boolean
|
||||
onUserInteracted?: () => void
|
||||
overflowAnchor?: "none" | "auto" | "dynamic"
|
||||
bottomThreshold?: number
|
||||
}
|
||||
|
||||
export function createAutoScroll(options: AutoScrollOptions) {
|
||||
let scroll: HTMLElement | undefined
|
||||
let settling = false
|
||||
let settleTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let autoTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let stopTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let cleanup: (() => void) | undefined
|
||||
let auto: { time: number } | undefined
|
||||
|
||||
const threshold = () => options.bottomThreshold ?? 10
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
contentRef: undefined as HTMLElement | undefined,
|
||||
userScrolled: false,
|
||||
})
|
||||
|
||||
const active = () => options.working() || settling
|
||||
|
||||
const distanceFromBottom = (el: HTMLElement) => {
|
||||
return el.scrollHeight - el.clientHeight - el.scrollTop
|
||||
}
|
||||
|
||||
const canScroll = (el: HTMLElement) => {
|
||||
return el.scrollHeight - el.clientHeight > 1
|
||||
}
|
||||
|
||||
// Browsers can dispatch scroll events asynchronously. If new content arrives
|
||||
// between us calling `scrollTo()` and the subsequent `scroll` event firing,
|
||||
// the handler can see a non-zero `distanceFromBottom` and incorrectly assume
|
||||
// the user scrolled.
|
||||
const markAuto = (_el: HTMLElement) => {
|
||||
auto = { time: Date.now() }
|
||||
|
||||
if (autoTimer) clearTimeout(autoTimer)
|
||||
autoTimer = setTimeout(() => {
|
||||
auto = undefined
|
||||
autoTimer = undefined
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const isAuto = (_el: HTMLElement) => {
|
||||
const a = auto
|
||||
if (!a) return false
|
||||
|
||||
if (Date.now() - a.time > 250) {
|
||||
auto = undefined
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const scrollToBottomNow = (behavior: ScrollBehavior) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
markAuto(el)
|
||||
if (behavior === "smooth") {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior })
|
||||
return
|
||||
}
|
||||
|
||||
// `scrollTop` assignment bypasses any CSS `scroll-behavior: smooth`.
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
|
||||
const scrollToBottom = (force: boolean) => {
|
||||
if (!force && !active()) return
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
if (!force && store.userScrolled) return
|
||||
if (force && store.userScrolled) setStore("userScrolled", false)
|
||||
|
||||
const distance = distanceFromBottom(el)
|
||||
if (distance < 2) return
|
||||
|
||||
// For auto-following content we prefer immediate updates to avoid
|
||||
// visible "catch up" animations while content is still settling.
|
||||
scrollToBottomNow("auto")
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
if (!canScroll(el)) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
if (store.userScrolled) return
|
||||
|
||||
setStore("userScrolled", true)
|
||||
options.onUserInteracted?.()
|
||||
}
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY >= 0) return
|
||||
// If the user is scrolling within a nested scrollable region (tool output,
|
||||
// code block, etc), don't treat it as leaving the "follow bottom" mode.
|
||||
// Those regions opt in via `data-scrollable`.
|
||||
const el = scroll
|
||||
const target = e.target instanceof Element ? e.target : undefined
|
||||
const nested = target?.closest("[data-scrollable]")
|
||||
if (el && nested && nested !== el) return
|
||||
stop()
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
if (!canScroll(el)) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
|
||||
if (distanceFromBottom(el) < threshold()) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore scroll events triggered by our own scrollToBottom calls.
|
||||
if (!store.userScrolled && isAuto(el)) {
|
||||
scrollToBottom(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Debounce to avoid layout-induced scroll shifts (e.g. images loading,
|
||||
// virtual-list reflows) from incorrectly breaking auto-follow.
|
||||
if (stopTimer) clearTimeout(stopTimer)
|
||||
stopTimer = setTimeout(() => {
|
||||
stopTimer = undefined
|
||||
const cur = scroll
|
||||
if (!cur) return
|
||||
if (distanceFromBottom(cur) < threshold()) return
|
||||
if (!store.userScrolled && isAuto(cur)) return
|
||||
stop()
|
||||
}, DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const handleInteraction = () => {
|
||||
if (!active()) return
|
||||
stop()
|
||||
}
|
||||
|
||||
const updateOverflowAnchor = (el: HTMLElement) => {
|
||||
const mode = options.overflowAnchor ?? "dynamic"
|
||||
|
||||
if (mode === "none") {
|
||||
el.style.overflowAnchor = "none"
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === "auto") {
|
||||
el.style.overflowAnchor = "auto"
|
||||
return
|
||||
}
|
||||
|
||||
el.style.overflowAnchor = store.userScrolled ? "auto" : "none"
|
||||
}
|
||||
|
||||
createResizeObserver(
|
||||
() => store.contentRef,
|
||||
() => {
|
||||
const el = scroll
|
||||
if (el && !canScroll(el)) {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
return
|
||||
}
|
||||
if (!active()) return
|
||||
if (store.userScrolled) return
|
||||
// ResizeObserver fires after layout, before paint.
|
||||
// Keep the bottom locked in the same frame to avoid visible
|
||||
// "jump up then catch up" artifacts while streaming content.
|
||||
scrollToBottom(false)
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(options.working, (working: boolean) => {
|
||||
settling = false
|
||||
if (settleTimer) clearTimeout(settleTimer)
|
||||
settleTimer = undefined
|
||||
|
||||
if (working) {
|
||||
scrollToBottom(true)
|
||||
return
|
||||
}
|
||||
|
||||
settling = true
|
||||
settleTimer = setTimeout(() => {
|
||||
settling = false
|
||||
}, 300)
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
// Track `userScrolled` even before `scrollRef` is attached, so we can
|
||||
// update overflow anchoring once the element exists.
|
||||
store.userScrolled
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
updateOverflowAnchor(el)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (settleTimer) clearTimeout(settleTimer)
|
||||
if (autoTimer) clearTimeout(autoTimer)
|
||||
if (stopTimer) clearTimeout(stopTimer)
|
||||
if (cleanup) cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
scrollRef: (el: HTMLElement | undefined) => {
|
||||
if (cleanup) {
|
||||
cleanup()
|
||||
cleanup = undefined
|
||||
}
|
||||
|
||||
scroll = el
|
||||
|
||||
if (!el) return
|
||||
|
||||
updateOverflowAnchor(el)
|
||||
el.addEventListener("wheel", handleWheel, { passive: true })
|
||||
|
||||
cleanup = () => {
|
||||
el.removeEventListener("wheel", handleWheel)
|
||||
}
|
||||
},
|
||||
contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el),
|
||||
handleScroll,
|
||||
handleInteraction,
|
||||
pause: stop,
|
||||
resume: () => {
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
scrollToBottom(true)
|
||||
},
|
||||
scrollToBottom: () => scrollToBottom(false),
|
||||
forceScrollToBottom: () => scrollToBottom(true),
|
||||
userScrolled: () => store.userScrolled,
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "@opencode-ai/ui/hooks"
|
||||
export { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
export * from "./create-auto-scroll"
|
||||
|
||||
@@ -54,6 +54,7 @@ html[data-theme="kilo-vscode"] {
|
||||
|
||||
--surface-interactive-base: var(--vscode-list-activeSelectionBackground);
|
||||
--surface-interactive-hover: var(--vscode-list-hoverBackground);
|
||||
--surface-interactive-active: var(--vscode-list-activeSelectionBackground);
|
||||
--surface-interactive-weak: var(--vscode-list-inactiveSelectionBackground);
|
||||
--surface-interactive-weak-hover: var(--vscode-list-hoverBackground);
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ You’re free to use, modify, and distribute this code, including for commercial
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome, and they are greatly appreciated! Get started by reading our [Contributing Guide](CONTRIBUTING.md). Or join our [Discord](https://discord.gg/kilocode) to chat with the team and community.
|
||||
Contributions are welcome, and they are greatly appreciated! Get started by reading our [Contributing Guide](CONTRIBUTING.md). Or join our [Discord](https://kilo.ai/discord) to chat with the team and community.
|
||||
|
||||
Thanks to all the contributors who help make Kilo better!
|
||||
|
||||
|
||||
@@ -139,6 +139,33 @@ const cssPackageResolvePlugin = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBrowserWebviewContext(entryPoint, outfile) {
|
||||
return esbuild.context({
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: "browser",
|
||||
outfile,
|
||||
logLevel: "silent",
|
||||
loader: {
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
".ttf": "file",
|
||||
},
|
||||
plugins: [
|
||||
solidDedupePlugin,
|
||||
pierreWorkerStubPlugin,
|
||||
svgSpritePlugin,
|
||||
cssPackageResolvePlugin,
|
||||
solidPlugin(),
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Build extension
|
||||
const extensionCtx = await esbuild.context({
|
||||
@@ -156,62 +183,32 @@ async function main() {
|
||||
})
|
||||
|
||||
// Build Agent Manager webview (SolidJS, shares components with sidebar)
|
||||
const agentManagerCtx = await esbuild.context({
|
||||
entryPoints: ["webview-ui/agent-manager/index.tsx"],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: "browser",
|
||||
outfile: "dist/agent-manager.js",
|
||||
logLevel: "silent",
|
||||
loader: {
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
".ttf": "file",
|
||||
},
|
||||
plugins: [
|
||||
solidDedupePlugin,
|
||||
pierreWorkerStubPlugin,
|
||||
svgSpritePlugin,
|
||||
cssPackageResolvePlugin,
|
||||
solidPlugin(),
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
})
|
||||
const agentManagerCtx = await createBrowserWebviewContext(
|
||||
"webview-ui/agent-manager/index.tsx",
|
||||
"dist/agent-manager.js",
|
||||
)
|
||||
|
||||
// Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components)
|
||||
const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js")
|
||||
|
||||
// Build webview
|
||||
const webviewCtx = await esbuild.context({
|
||||
entryPoints: ["webview-ui/src/index.tsx"],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: "browser",
|
||||
outfile: "dist/webview.js",
|
||||
logLevel: "silent",
|
||||
loader: {
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
".ttf": "file",
|
||||
},
|
||||
plugins: [
|
||||
solidDedupePlugin,
|
||||
pierreWorkerStubPlugin,
|
||||
svgSpritePlugin,
|
||||
cssPackageResolvePlugin,
|
||||
solidPlugin(),
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
})
|
||||
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
|
||||
|
||||
if (watch) {
|
||||
await Promise.all([extensionCtx.watch(), webviewCtx.watch(), agentManagerCtx.watch()])
|
||||
await Promise.all([extensionCtx.watch(), webviewCtx.watch(), agentManagerCtx.watch(), diffViewerCtx.watch()])
|
||||
} else {
|
||||
await Promise.all([extensionCtx.rebuild(), webviewCtx.rebuild(), agentManagerCtx.rebuild()])
|
||||
await Promise.all([extensionCtx.dispose(), webviewCtx.dispose(), agentManagerCtx.dispose()])
|
||||
await Promise.all([
|
||||
extensionCtx.rebuild(),
|
||||
webviewCtx.rebuild(),
|
||||
agentManagerCtx.rebuild(),
|
||||
diffViewerCtx.rebuild(),
|
||||
])
|
||||
await Promise.all([
|
||||
extensionCtx.dispose(),
|
||||
webviewCtx.dispose(),
|
||||
agentManagerCtx.dispose(),
|
||||
diffViewerCtx.dispose(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import eslintConfigPrettier from "eslint-config-prettier"
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
@@ -28,6 +28,7 @@ export default [
|
||||
curly: "warn",
|
||||
eqeqeq: "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"max-lines": ["error", 3000],
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": [
|
||||
"src/extension.ts",
|
||||
"webview-ui/agent-manager/index.tsx",
|
||||
"webview-ui/diff-viewer/index.tsx",
|
||||
"webview-ui/src/index.tsx",
|
||||
"src/**/__tests__/**/*.{ts,spec.ts}",
|
||||
"src/**/*.test.ts",
|
||||
"tests/**/*.{ts,mts}",
|
||||
"script/*.ts"
|
||||
],
|
||||
"project": ["src/**/*.ts", "webview-ui/**/*.{ts,tsx}"],
|
||||
"ignore": ["src/services/autocomplete/**"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"exclude": ["dependencies", "devDependencies", "optionalPeerDependencies", "unlisted", "unresolved", "binaries"]
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.0.40",
|
||||
"version": "7.0.43",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
@@ -110,6 +110,11 @@
|
||||
"dark": "assets/icons/kilo-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.showChanges",
|
||||
"title": "Show Changes",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.openMigrationWizard",
|
||||
"title": "Migrate Settings from Legacy Version",
|
||||
@@ -605,7 +610,7 @@
|
||||
},
|
||||
"kilo-code.new.model.modelID": {
|
||||
"type": "string",
|
||||
"default": "kilo-auto/frontier",
|
||||
"default": "kilo-auto/free",
|
||||
"description": "Default model ID for new sessions"
|
||||
},
|
||||
"kilo-code.new.autocomplete.enableAutoTrigger": {
|
||||
@@ -697,7 +702,8 @@
|
||||
"check-types": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint src",
|
||||
"knip": "knip",
|
||||
"lint": "eslint src webview-ui",
|
||||
"test": "vscode-test",
|
||||
"test:unit": "bun test tests/unit/",
|
||||
"rebuild-sdk": "bun run --cwd ../sdk/js build",
|
||||
@@ -716,10 +722,12 @@
|
||||
"@types/vscode": "^1.108.0",
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.7.1",
|
||||
"esbuild": "^0.27.2",
|
||||
"esbuild-plugin-solid": "^0.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"knip": "5.85.0",
|
||||
"prettier": "^3.8.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"storybook": "10.2.10",
|
||||
@@ -728,8 +736,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@vscode/vsce": "^3.7.1"
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
|
||||
@@ -29,4 +29,7 @@ try {
|
||||
}
|
||||
|
||||
const vsix = (await $`ls -1v ${outDir}/*.vsix`.text()).trim().split("\n").at(-1)!
|
||||
await $`code --force --install-extension ${vsix}`
|
||||
const execPath = process.env.VSCODE_EXEC_PATH ?? ""
|
||||
const cli = execPath.toLowerCase().includes("insiders") ? "code-insiders" : "code"
|
||||
console.log(`Installing into: ${cli} (VSCODE_EXEC_PATH=${execPath || "<not set>"})`)
|
||||
await $`${cli} --force --install-extension ${vsix}`
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { FileDiff } from "@kilocode/sdk/v2/client"
|
||||
import type { KiloConnectionService } from "./services/cli-backend"
|
||||
import { buildWebviewHtml } from "./utils"
|
||||
import { GitOps } from "./agent-manager/GitOps"
|
||||
import {
|
||||
appendOutput,
|
||||
getWorkspaceRoot,
|
||||
hashFileDiffs,
|
||||
openWorkspaceRelativeFile,
|
||||
resolveLocalDiffTarget,
|
||||
} from "./review-utils"
|
||||
|
||||
/**
|
||||
* DiffViewerProvider opens a full-screen diff viewer in an editor tab.
|
||||
* It shows the local workspace diff and forwards review comments back to the sidebar chat.
|
||||
*/
|
||||
export class DiffViewerProvider implements vscode.Disposable {
|
||||
public static readonly viewType = "kilo-code.new.DiffViewerPanel"
|
||||
|
||||
private panel: vscode.WebviewPanel | undefined
|
||||
private diffInterval: ReturnType<typeof setInterval> | undefined
|
||||
private lastDiffHash: string | undefined
|
||||
private cachedDiffTarget: { directory: string; baseBranch: string } | undefined
|
||||
private gitOps: GitOps
|
||||
private outputChannel: vscode.OutputChannel
|
||||
private onSendComments: ((comments: unknown[]) => void) | undefined
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly connectionService: KiloConnectionService,
|
||||
) {
|
||||
this.gitOps = new GitOps({ log: (...args) => this.log(...args) })
|
||||
this.outputChannel = vscode.window.createOutputChannel("Kilo Diff Viewer")
|
||||
}
|
||||
|
||||
private log(...args: unknown[]) {
|
||||
appendOutput(this.outputChannel, "DiffViewer", ...args)
|
||||
}
|
||||
|
||||
public setCommentHandler(handler: (comments: unknown[]) => void): void {
|
||||
this.onSendComments = handler
|
||||
}
|
||||
|
||||
public openPanel(): void {
|
||||
if (this.panel) {
|
||||
this.panel.reveal(vscode.ViewColumn.One)
|
||||
return
|
||||
}
|
||||
|
||||
this.panel = vscode.window.createWebviewPanel(DiffViewerProvider.viewType, "Changes", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [this.extensionUri],
|
||||
})
|
||||
|
||||
this.panel.iconPath = {
|
||||
light: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-light.svg"),
|
||||
dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
|
||||
}
|
||||
|
||||
this.panel.webview.onDidReceiveMessage((msg) => this.onMessage(msg), undefined, [])
|
||||
this.panel.webview.html = this.getHtml(this.panel.webview)
|
||||
|
||||
this.panel.onDidDispose(() => {
|
||||
this.log("Panel disposed")
|
||||
this.stopDiffPolling()
|
||||
this.panel = undefined
|
||||
})
|
||||
}
|
||||
|
||||
private onMessage(msg: Record<string, unknown>): void {
|
||||
const type = msg.type as string
|
||||
|
||||
if (type === "webviewReady") {
|
||||
this.post({
|
||||
type: "ready",
|
||||
vscodeLanguage: vscode.env.language,
|
||||
languageOverride: vscode.workspace.getConfiguration("kilo-code.new").get<string>("language"),
|
||||
workspaceDirectory: getWorkspaceRoot(),
|
||||
})
|
||||
this.startDiffPolling()
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "diffViewer.sendComments" && Array.isArray(msg.comments)) {
|
||||
this.onSendComments?.(msg.comments)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "diffViewer.close") {
|
||||
this.panel?.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "diffViewer.setDiffStyle" && (msg.style === "unified" || msg.style === "split")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "openFile" && typeof msg.filePath === "string") {
|
||||
openWorkspaceRelativeFile(msg.filePath, typeof msg.line === "number" ? msg.line : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveLocalDiffTarget(): Promise<{ directory: string; baseBranch: string } | undefined> {
|
||||
return await resolveLocalDiffTarget(this.gitOps, (...args) => this.log(...args))
|
||||
}
|
||||
|
||||
private async initialFetch(): Promise<void> {
|
||||
this.post({ type: "diffViewer.loading", loading: true })
|
||||
|
||||
const target = await this.resolveLocalDiffTarget()
|
||||
if (!target) {
|
||||
this.post({ type: "diffViewer.diffs", diffs: [] })
|
||||
this.post({ type: "diffViewer.loading", loading: false })
|
||||
return
|
||||
}
|
||||
|
||||
this.cachedDiffTarget = target
|
||||
|
||||
try {
|
||||
await this.connectionService.connect(target.directory)
|
||||
const client = this.connectionService.getClient()
|
||||
const { data: diffs } = await client.worktree.diff(
|
||||
{ directory: target.directory, base: target.baseBranch },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
this.lastDiffHash = hashFileDiffs(diffs)
|
||||
|
||||
this.log(`Initial diff: ${diffs.length} file(s)`)
|
||||
this.post({ type: "diffViewer.diffs", diffs })
|
||||
} catch (err) {
|
||||
this.log("Failed to fetch initial diff:", err)
|
||||
} finally {
|
||||
this.post({ type: "diffViewer.loading", loading: false })
|
||||
}
|
||||
}
|
||||
|
||||
private async pollDiff(): Promise<void> {
|
||||
const target = this.cachedDiffTarget
|
||||
if (!target) {
|
||||
await this.initialFetch()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.connectionService.getClient()
|
||||
const { data: diffs } = await client.worktree.diff(
|
||||
{ directory: target.directory, base: target.baseBranch },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
const hash = hashFileDiffs(diffs)
|
||||
|
||||
if (hash === this.lastDiffHash) return
|
||||
this.lastDiffHash = hash
|
||||
this.post({ type: "diffViewer.diffs", diffs })
|
||||
} catch (err) {
|
||||
this.log("Failed to poll diff:", err)
|
||||
}
|
||||
}
|
||||
|
||||
private startDiffPolling(): void {
|
||||
this.stopDiffPolling()
|
||||
this.lastDiffHash = undefined
|
||||
this.cachedDiffTarget = undefined
|
||||
|
||||
void this.initialFetch().then(() => {
|
||||
if (!this.panel) return
|
||||
this.diffInterval = setInterval(() => {
|
||||
void this.pollDiff()
|
||||
}, 2500)
|
||||
})
|
||||
}
|
||||
|
||||
private stopDiffPolling(): void {
|
||||
if (this.diffInterval) {
|
||||
clearInterval(this.diffInterval)
|
||||
this.diffInterval = undefined
|
||||
}
|
||||
|
||||
this.lastDiffHash = undefined
|
||||
this.cachedDiffTarget = undefined
|
||||
}
|
||||
|
||||
private post(message: Record<string, unknown>): void {
|
||||
if (this.panel?.webview) void this.panel.webview.postMessage(message)
|
||||
}
|
||||
|
||||
private getHtml(webview: vscode.Webview): string {
|
||||
return buildWebviewHtml(webview, {
|
||||
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.js")),
|
||||
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.css")),
|
||||
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
|
||||
title: "Changes",
|
||||
port: this.connectionService.getServerInfo()?.port,
|
||||
extraStyles: "#root { display: flex; flex-direction: column; }",
|
||||
})
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.stopDiffPolling()
|
||||
this.panel?.dispose()
|
||||
this.outputChannel.dispose()
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private cachedConfigMessage: unknown = null
|
||||
/** Cached notificationsLoaded payload */
|
||||
private cachedNotificationsMessage: unknown = null
|
||||
private pendingReviewComments: unknown[][] = []
|
||||
|
||||
private trackedSessionIds: Set<string> = new Set()
|
||||
private syncedChildSessions: Set<string> = new Set()
|
||||
@@ -316,6 +317,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
console.log("[Kilo New] KiloProvider: ✅ webviewReady received")
|
||||
this.isWebviewReady = true
|
||||
await this.syncWebviewState("webviewReady")
|
||||
this.flushPendingReviewComments()
|
||||
break
|
||||
case "sendMessage": {
|
||||
const files = z
|
||||
@@ -389,6 +391,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "openChanges":
|
||||
vscode.commands.executeCommand("kilo-code.new.showChanges")
|
||||
break
|
||||
case "openFile":
|
||||
if (message.filePath) {
|
||||
this.handleOpenFile(message.filePath, message.line, message.column)
|
||||
@@ -642,6 +647,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
await this.syncWebviewState("sse-connected")
|
||||
await this.flushPendingSessionRefresh("sse-connected")
|
||||
await this.fetchAndSendPendingPermissions()
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Failed during connected state handling:", error)
|
||||
this.postMessage({
|
||||
@@ -820,6 +826,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
sessionID,
|
||||
messages,
|
||||
})
|
||||
|
||||
// Recover any permission.asked events that were missed while the webview
|
||||
// was loading or during an SSE reconnection (fire-and-forget).
|
||||
void this.fetchAndSendPendingPermissions()
|
||||
} catch (error) {
|
||||
// Silently ignore aborted requests — the user switched to a different session
|
||||
if (abort.signal.aborted) return
|
||||
@@ -1003,7 +1013,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
const config = vscode.workspace.getConfiguration("kilo-code.new.model")
|
||||
const providerID = config.get<string>("providerID", "kilo")
|
||||
const modelID = config.get<string>("modelID", "kilo-auto/frontier")
|
||||
const modelID = config.get<string>("modelID", "kilo-auto/free")
|
||||
|
||||
const message = {
|
||||
type: "providersLoaded",
|
||||
@@ -1510,23 +1520,58 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
response: "once" | "always" | "reject",
|
||||
): Promise<void> {
|
||||
if (!this.client) {
|
||||
this.postMessage({ type: "permissionError", permissionID: permissionId })
|
||||
return
|
||||
}
|
||||
|
||||
const targetSessionID = sessionID || this.currentSession?.id
|
||||
if (!targetSessionID) {
|
||||
console.error("[Kilo New] KiloProvider: No sessionID for permission response")
|
||||
this.postMessage({ type: "permissionError", permissionID: permissionId })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceDir = this.getWorkspaceDirectory(targetSessionID)
|
||||
await this.client.permission.respond(
|
||||
{ sessionID: targetSessionID, permissionID: permissionId, response, directory: workspaceDir },
|
||||
await this.client.permission.reply(
|
||||
{ requestID: permissionId, reply: response, directory: workspaceDir },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to respond to permission:", error)
|
||||
this.postMessage({ type: "permissionError", permissionID: permissionId })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all pending permissions from the backend and forward any that belong
|
||||
* to tracked sessions to the webview. Called after SSE reconnects and after
|
||||
* loading messages for a session so that missed permission.asked events are
|
||||
* recovered instead of leaving the server blocked indefinitely.
|
||||
*/
|
||||
private async fetchAndSendPendingPermissions(): Promise<void> {
|
||||
if (!this.client) return
|
||||
try {
|
||||
const workspaceDir = this.getWorkspaceDirectory()
|
||||
const { data } = await this.client.permission.list({ directory: workspaceDir })
|
||||
if (!data) return
|
||||
for (const perm of data) {
|
||||
if (!this.trackedSessionIds.has(perm.sessionID)) continue
|
||||
this.postMessage({
|
||||
type: "permissionRequest",
|
||||
permission: {
|
||||
id: perm.id,
|
||||
sessionID: perm.sessionID,
|
||||
toolName: perm.permission,
|
||||
patterns: perm.patterns,
|
||||
args: perm.metadata,
|
||||
message: `Permission required: ${perm.permission}`,
|
||||
tool: perm.tool,
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to fetch pending permissions:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1923,6 +1968,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
})
|
||||
}
|
||||
|
||||
public async appendReviewComments(comments: unknown[]): Promise<void> {
|
||||
this.pendingReviewComments.push(comments)
|
||||
|
||||
if (!this.webview) {
|
||||
await vscode.commands.executeCommand(`${KiloProvider.viewType}.focus`)
|
||||
}
|
||||
|
||||
this.flushPendingReviewComments()
|
||||
}
|
||||
|
||||
private flushPendingReviewComments(): void {
|
||||
if (!this.webview || !this.isWebviewReady || this.pendingReviewComments.length === 0) return
|
||||
|
||||
const pending = this.pendingReviewComments
|
||||
this.pendingReviewComments = []
|
||||
|
||||
for (const comments of pending) {
|
||||
this.postMessage({ type: "appendReviewComments", comments })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the git remote URL for the current workspace using VS Code's built-in Git API.
|
||||
* Returns undefined if not in a git repo or no remotes are configured.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as vscode from "vscode"
|
||||
import { KiloProvider } from "./KiloProvider"
|
||||
import type { KiloConnectionService } from "./services/cli-backend"
|
||||
|
||||
type PanelView = "settings" | "profile"
|
||||
|
||||
/**
|
||||
* Opens Settings or Profile as an editor-area WebviewPanel,
|
||||
* keeping the sidebar chat undisturbed.
|
||||
*
|
||||
* Each view type is a singleton panel — calling openPanel() again
|
||||
* reveals the existing panel instead of creating a duplicate.
|
||||
*
|
||||
* Uses a full KiloProvider under the hood so Settings/Profile have
|
||||
* the same backend connectivity (config, providers, profile, auth)
|
||||
* as the sidebar.
|
||||
*/
|
||||
export class SettingsEditorProvider implements vscode.Disposable {
|
||||
private panels = new Map<PanelView, vscode.WebviewPanel>()
|
||||
private providers = new Map<PanelView, KiloProvider>()
|
||||
|
||||
constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
private readonly connectionService: KiloConnectionService,
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
) {}
|
||||
|
||||
openPanel(view: PanelView): void {
|
||||
const existing = this.panels.get(view)
|
||||
if (existing) {
|
||||
existing.reveal(vscode.ViewColumn.One)
|
||||
return
|
||||
}
|
||||
|
||||
const title = view === "settings" ? "Kilo Settings" : "Kilo Profile"
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(`kilo-code.new.${view}Panel`, title, vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [this.extensionUri],
|
||||
})
|
||||
|
||||
panel.iconPath = {
|
||||
light: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-light.svg"),
|
||||
dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
|
||||
}
|
||||
|
||||
// Create a dedicated KiloProvider for this panel so it has full
|
||||
// backend connectivity (config, providers, agents, profile, auth).
|
||||
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context)
|
||||
provider.resolveWebviewPanel(panel)
|
||||
|
||||
// Listen for closePanel from the webview (back button in panel mode)
|
||||
const closePanelDisposable = panel.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg.type === "closePanel") {
|
||||
panel.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
// Once the webview signals ready, navigate to the target view.
|
||||
const readyDisposable = panel.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg.type === "webviewReady") {
|
||||
// Small delay to let KiloProvider's own webviewReady handler finish first
|
||||
setTimeout(() => {
|
||||
provider.postMessage({ type: "navigate", view })
|
||||
}, 50)
|
||||
readyDisposable.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
this.panels.set(view, panel)
|
||||
this.providers.set(view, provider)
|
||||
|
||||
panel.onDidDispose(() => {
|
||||
console.log(`[Kilo New] ${title} panel disposed`)
|
||||
closePanelDisposable.dispose()
|
||||
provider.dispose()
|
||||
this.panels.delete(view)
|
||||
this.providers.delete(view)
|
||||
})
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const [, panel] of this.panels) {
|
||||
panel.dispose()
|
||||
}
|
||||
this.panels.clear()
|
||||
this.providers.clear()
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { isAbsolutePath } from "../path-utils"
|
||||
import { KiloProvider } from "../KiloProvider"
|
||||
import { buildWebviewHtml } from "../utils"
|
||||
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
|
||||
import { WorktreeStateManager } from "./WorktreeStateManager"
|
||||
import { WorktreeStateManager, remoteRef } from "./WorktreeStateManager"
|
||||
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
|
||||
import { GitStatsPoller, type WorktreePresenceResult } from "./GitStatsPoller"
|
||||
import { GitOps, type ApplyConflict } from "./GitOps"
|
||||
@@ -17,9 +17,12 @@ import { normalizePath } from "./git-import"
|
||||
import { SetupScriptService } from "./SetupScriptService"
|
||||
import { SetupScriptRunner } from "./SetupScriptRunner"
|
||||
import { SessionTerminalManager } from "./SessionTerminalManager"
|
||||
import { createTerminalHost } from "./terminal-host"
|
||||
import { executeVscodeTask } from "./task-runner"
|
||||
import { formatKeybinding } from "./format-keybinding"
|
||||
import { TelemetryProxy, TelemetryEventName } from "../services/telemetry"
|
||||
import { MAX_MULTI_VERSIONS } from "./constants"
|
||||
import { getWorkspaceRoot, hashFileDiffs, openFileInEditor, resolveLocalDiffTarget } from "../review-utils"
|
||||
|
||||
/**
|
||||
* AgentManagerProvider opens the Agent Manager panel.
|
||||
@@ -64,8 +67,9 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
private readonly connectionService: KiloConnectionService,
|
||||
) {
|
||||
this.outputChannel = vscode.window.createOutputChannel("Kilo Agent Manager")
|
||||
this.terminalManager = new SessionTerminalManager((msg) =>
|
||||
this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
|
||||
this.terminalManager = new SessionTerminalManager(
|
||||
(msg) => this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
|
||||
createTerminalHost(),
|
||||
)
|
||||
this.gitOps = new GitOps({ log: (...args) => this.log(...args) })
|
||||
this.statsPoller = new GitStatsPoller({
|
||||
@@ -475,6 +479,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
branch: result.branch,
|
||||
path: result.path,
|
||||
parentBranch: result.parentBranch,
|
||||
remote: result.remote,
|
||||
groupId: opts?.groupId,
|
||||
label: opts?.label,
|
||||
})
|
||||
@@ -1047,6 +1052,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
branch: result.branch,
|
||||
path: result.path,
|
||||
parentBranch: result.parentBranch,
|
||||
remote: result.remote,
|
||||
})
|
||||
this.pushState()
|
||||
|
||||
@@ -1112,6 +1118,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
branch: result.branch,
|
||||
path: result.path,
|
||||
parentBranch: result.parentBranch,
|
||||
remote: result.remote,
|
||||
})
|
||||
this.pushState()
|
||||
|
||||
@@ -1186,8 +1193,8 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
return
|
||||
}
|
||||
|
||||
const parent = await manager.defaultBranch()
|
||||
worktree = state.addWorktree({ branch, path: wtPath, parentBranch: parent })
|
||||
const base = await manager.resolveBaseBranch()
|
||||
worktree = state.addWorktree({ branch, path: wtPath, parentBranch: base.branch, remote: base.remote })
|
||||
this.pushState()
|
||||
|
||||
const session = await this.createSessionInWorktree(wtPath, branch, worktree.id)
|
||||
@@ -1215,7 +1222,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
mode: "worktree",
|
||||
branch,
|
||||
path: wtPath,
|
||||
parentBranch: parent,
|
||||
parentBranch: base.branch,
|
||||
})
|
||||
this.postToWebview({ type: "agentManager.importResult", success: true, message: `Imported ${branch}` })
|
||||
this.log(`Imported external worktree ${wtPath} (${branch})`)
|
||||
@@ -1261,10 +1268,15 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
}
|
||||
|
||||
let imported = 0
|
||||
const parent = await manager.defaultBranch()
|
||||
const base = await manager.resolveBaseBranch()
|
||||
for (const ext of externals) {
|
||||
try {
|
||||
const worktree = state.addWorktree({ branch: ext.branch, path: ext.path, parentBranch: parent })
|
||||
const worktree = state.addWorktree({
|
||||
branch: ext.branch,
|
||||
path: ext.path,
|
||||
parentBranch: base.branch,
|
||||
remote: base.remote,
|
||||
})
|
||||
const session = await this.createSessionInWorktree(ext.path, ext.branch, worktree.id)
|
||||
if (session) {
|
||||
state.addSession(session.id, worktree.id)
|
||||
@@ -1343,7 +1355,13 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
const service = this.getSetupScriptService()
|
||||
if (!service) return
|
||||
try {
|
||||
await service.openInEditor()
|
||||
if (!service.hasScript()) {
|
||||
await service.createDefaultScript()
|
||||
}
|
||||
const resolved = service.resolveScript()
|
||||
if (!resolved) return
|
||||
const document = await vscode.workspace.openTextDocument(resolved.path)
|
||||
await vscode.window.showTextDocument(document)
|
||||
} catch (error) {
|
||||
this.log(`Failed to open setup script: ${error}`)
|
||||
}
|
||||
@@ -1363,7 +1381,11 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
branch,
|
||||
worktreeId,
|
||||
})
|
||||
const runner = new SetupScriptRunner(this.outputChannel, service)
|
||||
const runner = new SetupScriptRunner(
|
||||
(msg) => this.outputChannel.appendLine(`[SetupScriptRunner] ${msg}`),
|
||||
service,
|
||||
executeVscodeTask,
|
||||
)
|
||||
await runner.runIfConfigured({ worktreePath, repoPath: root })
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
@@ -1484,9 +1506,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getWorkspaceRoot(): string | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders
|
||||
if (folders && folders.length > 0) return folders[0].uri.fsPath
|
||||
return undefined
|
||||
return getWorkspaceRoot()
|
||||
}
|
||||
|
||||
private getWorktreeManager(): WorktreeManager | undefined {
|
||||
@@ -1573,7 +1593,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
|
||||
try {
|
||||
this.postApplyResult(worktreeId, "checking", "Checking for conflicts...")
|
||||
const patch = await this.gitOps.buildWorktreePatch(worktree.path, worktree.parentBranch, selectedFiles)
|
||||
const patch = await this.gitOps.buildWorktreePatch(worktree.path, remoteRef(worktree), selectedFiles)
|
||||
|
||||
if (!patch.trim()) {
|
||||
this.postApplyResult(worktreeId, "success", "No changes to apply")
|
||||
@@ -1632,15 +1652,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
private openWorktreeFile(sessionId: string, filePath: string, line?: number, column?: number): void {
|
||||
if (isAbsolutePath(filePath)) {
|
||||
const uri = vscode.Uri.file(filePath)
|
||||
const options: vscode.TextDocumentShowOptions = { preview: true }
|
||||
if (line !== undefined && line > 0) {
|
||||
const col = column !== undefined && column > 0 ? column - 1 : 0
|
||||
options.selection = new vscode.Range(new vscode.Position(line - 1, col), new vscode.Position(line - 1, col))
|
||||
}
|
||||
vscode.workspace.openTextDocument(uri).then(
|
||||
(doc) => vscode.window.showTextDocument(doc, options),
|
||||
(err) => console.error("[Kilo New] AgentManagerProvider: Failed to open file:", uri.fsPath, err),
|
||||
)
|
||||
openFileInEditor(uri.fsPath, line, column, vscode.ViewColumn.Active, "AgentManagerProvider")
|
||||
return
|
||||
}
|
||||
const state = this.getStateManager()
|
||||
@@ -1660,16 +1672,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
console.error("[Kilo New] AgentManagerProvider: Cannot resolve file path:", err)
|
||||
return
|
||||
}
|
||||
const uri = vscode.Uri.file(resolved)
|
||||
const options: vscode.TextDocumentShowOptions = { preview: true }
|
||||
const target = Math.max(1, Math.floor(line ?? 1))
|
||||
const col = column !== undefined && column > 0 ? column - 1 : 0
|
||||
const pos = new vscode.Position(target - 1, col)
|
||||
options.selection = new vscode.Range(pos, pos)
|
||||
vscode.workspace.openTextDocument(uri).then(
|
||||
(doc) => vscode.window.showTextDocument(doc, options),
|
||||
(err) => console.error("[Kilo New] AgentManagerProvider: Failed to open file:", uri.fsPath, err),
|
||||
)
|
||||
openFileInEditor(resolved, line, column, vscode.ViewColumn.Active, "AgentManagerProvider")
|
||||
}
|
||||
|
||||
/** Resolve worktree path + parentBranch for a session, or undefined if not applicable. */
|
||||
@@ -1696,30 +1699,15 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
this.log(`resolveDiffTarget: worktree ${session.worktreeId} not found for session ${sessionId}`)
|
||||
return undefined
|
||||
}
|
||||
return { directory: worktree.path, baseBranch: worktree.parentBranch }
|
||||
// Always construct remote-prefixed ref for diff (e.g. "origin/main")
|
||||
return { directory: worktree.path, baseBranch: remoteRef(worktree) }
|
||||
}
|
||||
|
||||
/** Resolve diff target for the local workspace — diffs against the remote tracking
|
||||
* branch, falling back to the repo's default branch, and ultimately to HEAD so
|
||||
* local-only repos (no remote) still show working-tree changes in the diff panel. */
|
||||
private async resolveLocalDiffTarget(): Promise<{ directory: string; baseBranch: string } | undefined> {
|
||||
const root = this.getWorkspaceRoot()
|
||||
if (!root) {
|
||||
this.log("Local diff: no workspace root")
|
||||
return undefined
|
||||
}
|
||||
const branch = await this.gitOps.currentBranch(root)
|
||||
if (!branch || branch === "HEAD") {
|
||||
this.log("Local diff: detached HEAD or no branch")
|
||||
return undefined
|
||||
}
|
||||
const tracking = await this.gitOps.resolveTrackingBranch(root, branch)
|
||||
const defaultBranch = tracking ? undefined : await this.gitOps.resolveDefaultBranch(root, branch)
|
||||
const base = tracking ?? defaultBranch ?? "HEAD"
|
||||
this.log(
|
||||
`Local diff: branch=${branch} tracking=${tracking ?? "none"} default=${defaultBranch ?? "none"} base=${base}`,
|
||||
)
|
||||
return { directory: root, baseBranch: base }
|
||||
return await resolveLocalDiffTarget(this.gitOps, (...args) => this.log(...args))
|
||||
}
|
||||
|
||||
/** One-shot diff fetch with loading indicators. Resolves target async, then fetches. */
|
||||
@@ -1749,9 +1737,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
|
||||
this.log(`Worktree diff returned ${diffs.length} file(s) for session ${sessionId}`)
|
||||
|
||||
const hash = diffs
|
||||
.map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`)
|
||||
.join("|")
|
||||
const hash = hashFileDiffs(diffs)
|
||||
this.lastDiffHash = hash
|
||||
this.diffSessionId = sessionId
|
||||
|
||||
@@ -1775,9 +1761,7 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
const hash = diffs
|
||||
.map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`)
|
||||
.join("|")
|
||||
const hash = hashFileDiffs(diffs)
|
||||
if (hash === this.lastDiffHash && this.diffSessionId === sessionId) return
|
||||
this.lastDiffHash = hash
|
||||
this.diffSessionId = sessionId
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from "fs/promises"
|
||||
import simpleGit from "simple-git"
|
||||
import { parseWorktreeList, normalizePath } from "./git-import"
|
||||
|
||||
export interface GitOpsOptions {
|
||||
interface GitOpsOptions {
|
||||
log: (...args: unknown[]) => void
|
||||
refreshMs?: number
|
||||
/** Override git command execution for testing. */
|
||||
@@ -17,13 +17,13 @@ export interface ApplyConflict {
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface ApplyCheckResult {
|
||||
interface ApplyCheckResult {
|
||||
ok: boolean
|
||||
conflicts: ApplyConflict[]
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ApplyPatchResult {
|
||||
interface ApplyPatchResult {
|
||||
ok: boolean
|
||||
conflicts: ApplyConflict[]
|
||||
message: string
|
||||
@@ -210,33 +210,14 @@ export class GitOps {
|
||||
|
||||
/**
|
||||
* Count commits ahead and behind using `rev-list --left-right --count`.
|
||||
* Tries the best available ref in order: upstream tracking branch →
|
||||
* remote/branch → remote/parentBranch → local parentBranch.
|
||||
* Callers are expected to pass a fully-qualified ref (e.g. "origin/main").
|
||||
* Pass `remote` explicitly to refresh the tracking ref before counting;
|
||||
* the remote is NOT inferred from the ref to avoid misinterpreting
|
||||
* branch names that contain slashes (e.g. "release/1.0").
|
||||
*/
|
||||
async aheadBehind(cwd: string, parentBranch: string): Promise<{ ahead: number; behind: number }> {
|
||||
const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "")
|
||||
const branch = await this.raw(["branch", "--show-current"], cwd).catch(() => "")
|
||||
const remote = await this.resolveRemote(cwd, branch)
|
||||
await this.refreshRemote(cwd, remote)
|
||||
|
||||
const ref = (() => {
|
||||
if (upstream) return upstream
|
||||
const remoteBranch = branch ? `${remote}/${branch}` : ""
|
||||
// hasRemoteRef is async, so we can't use it inline — resolve below
|
||||
return { remoteBranch, remoteParent: `${remote}/${parentBranch}`, parentBranch }
|
||||
})()
|
||||
|
||||
if (typeof ref === "string") {
|
||||
return this.parseLeftRight(cwd, ref)
|
||||
}
|
||||
|
||||
if (ref.remoteBranch && (await this.hasRemoteRef(cwd, ref.remoteBranch))) {
|
||||
return this.parseLeftRight(cwd, ref.remoteBranch)
|
||||
}
|
||||
if (await this.hasRemoteRef(cwd, ref.remoteParent)) {
|
||||
return this.parseLeftRight(cwd, ref.remoteParent)
|
||||
}
|
||||
return this.parseLeftRight(cwd, ref.parentBranch)
|
||||
async aheadBehind(cwd: string, base: string, remote?: string): Promise<{ ahead: number; behind: number }> {
|
||||
if (remote) await this.refreshRemote(cwd, remote)
|
||||
return this.parseLeftRight(cwd, base)
|
||||
}
|
||||
|
||||
private async parseLeftRight(cwd: string, ref: string): Promise<{ ahead: number; behind: number }> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import type { KiloClient, FileDiff } from "@kilocode/sdk/v2/client"
|
||||
import type { Worktree } from "./WorktreeStateManager"
|
||||
import { remoteRef, type Worktree } from "./WorktreeStateManager"
|
||||
import type { GitOps } from "./GitOps"
|
||||
import { normalizePath } from "./git-import"
|
||||
|
||||
export interface WorktreeStats {
|
||||
interface WorktreeStats {
|
||||
worktreeId: string
|
||||
files: number
|
||||
additions: number
|
||||
@@ -14,7 +14,7 @@ export interface WorktreeStats {
|
||||
behind: number
|
||||
}
|
||||
|
||||
export interface LocalStats {
|
||||
interface LocalStats {
|
||||
branch: string
|
||||
files: number
|
||||
additions: number
|
||||
@@ -147,9 +147,10 @@ export class GitStatsPoller {
|
||||
await Promise.all(
|
||||
active.map(async (wt) => {
|
||||
try {
|
||||
const base = remoteRef(wt)
|
||||
const [{ data: diffs }, ab] = await Promise.all([
|
||||
client.worktree.diff({ directory: wt.path, base: wt.parentBranch }, { throwOnError: true }),
|
||||
this.git.aheadBehind(wt.path, wt.parentBranch),
|
||||
client.worktree.diff({ directory: wt.path, base }, { throwOnError: true }),
|
||||
this.git.aheadBehind(wt.path, base, wt.remote),
|
||||
])
|
||||
const files = diffs.length
|
||||
const additions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.additions, 0)
|
||||
@@ -237,8 +238,8 @@ export class GitStatsPoller {
|
||||
if (!branch || branch === "HEAD") return
|
||||
|
||||
const tracking = await this.git.resolveTrackingBranch(root, branch)
|
||||
|
||||
const base = tracking ?? (await this.git.resolveDefaultBranch(root, branch))
|
||||
const remote = await this.git.resolveRemote(root, branch).catch(() => undefined)
|
||||
|
||||
let files: number
|
||||
let additions: number
|
||||
@@ -250,7 +251,7 @@ export class GitStatsPoller {
|
||||
this.options.log(`Local stats: using HTTP client with base=${base}`)
|
||||
const [{ data: diffs }, ab] = await Promise.all([
|
||||
client.worktree.diff({ directory: root, base }, { throwOnError: true }),
|
||||
this.git.aheadBehind(root, base),
|
||||
this.git.aheadBehind(root, base, remote),
|
||||
])
|
||||
files = diffs.length
|
||||
additions = diffs.reduce((sum: number, d: FileDiff) => sum + d.additions, 0)
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { WorktreeStateManager } from "./WorktreeStateManager"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TerminalHost — narrow interface for the VS Code capabilities this module
|
||||
// needs. Implemented by AgentManagerProvider using the real vscode API.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TerminalHandle {
|
||||
show(preserveFocus: boolean): void
|
||||
dispose(): void
|
||||
readonly exitStatus: { code?: number } | undefined
|
||||
}
|
||||
|
||||
export interface TerminalHost {
|
||||
createTerminal(opts: { cwd: string; name: string }): TerminalHandle
|
||||
activeTerminal(): TerminalHandle | undefined
|
||||
workspacePath(): string | undefined
|
||||
showWarning(msg: string): void
|
||||
setContext(key: string, value: boolean): void
|
||||
onTerminalClosed(cb: (handle: TerminalHandle) => void): Disposable
|
||||
onActiveTerminalChanged(cb: (handle: TerminalHandle | undefined) => void): Disposable
|
||||
registerCommand(id: string, handler: (...args: unknown[]) => Promise<unknown>): Disposable
|
||||
executeCommand(id: string, ...args: unknown[]): Promise<unknown>
|
||||
}
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages VS Code terminals for agent manager sessions.
|
||||
* Manages terminals for agent manager sessions.
|
||||
* Each session can have an associated terminal that opens in the session's worktree directory,
|
||||
* or the main workspace folder for local sessions.
|
||||
*/
|
||||
export class SessionTerminalManager {
|
||||
private static readonly LOCAL_KEY = "__local__"
|
||||
|
||||
private terminals = new Map<string, { terminal: vscode.Terminal; cwd: string }>()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private terminals = new Map<string, { terminal: TerminalHandle; cwd: string }>()
|
||||
private disposables: Disposable[] = []
|
||||
private commandHandlers = new Map<string, (...args: unknown[]) => Promise<unknown>>()
|
||||
private commandDisposables = new Map<string, vscode.Disposable>()
|
||||
private commandDisposables = new Map<string, Disposable>()
|
||||
private panelOpen = false
|
||||
|
||||
constructor(private log: (msg: string) => void) {
|
||||
constructor(
|
||||
private log: (msg: string) => void,
|
||||
private host: TerminalHost,
|
||||
) {
|
||||
this.disposables.push(
|
||||
vscode.window.onDidCloseTerminal((terminal) => {
|
||||
host.onTerminalClosed((terminal) => {
|
||||
for (const [sessionId, entry] of this.terminals) {
|
||||
if (entry.terminal !== terminal) continue
|
||||
this.terminals.delete(sessionId)
|
||||
@@ -26,10 +55,10 @@ export class SessionTerminalManager {
|
||||
}
|
||||
this.updateContextKey()
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTerminal((terminal) => {
|
||||
host.onActiveTerminalChanged((terminal) => {
|
||||
const managed = terminal ? this.isManaged(terminal) : false
|
||||
if (terminal) this.panelOpen = true
|
||||
void vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
|
||||
void host.setContext("kilo-code.agentTerminalFocus", managed)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -59,13 +88,13 @@ export class SessionTerminalManager {
|
||||
// If terminal already exists, just focus it
|
||||
if (this.showExisting(sessionId, false)) return
|
||||
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const workspacePath = this.host.workspacePath()
|
||||
const worktreePath = state?.directoryFor(sessionId)
|
||||
const cwd = worktreePath ?? workspacePath
|
||||
|
||||
if (!cwd) {
|
||||
this.log(`showTerminal: no cwd resolved for session ${sessionId}`)
|
||||
vscode.window.showWarningMessage("Open a folder that contains a git repository to use worktrees")
|
||||
this.host.showWarning("Open a folder that contains a git repository to use worktrees")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,10 +112,10 @@ export class SessionTerminalManager {
|
||||
showLocalTerminal(): void {
|
||||
if (this.showExisting(SessionTerminalManager.LOCAL_KEY, false)) return
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const cwd = this.host.workspacePath()
|
||||
if (!cwd) {
|
||||
this.log("showLocalTerminal: no workspace folder open")
|
||||
vscode.window.showWarningMessage("Open a folder to use the local terminal")
|
||||
this.host.showWarning("Open a folder to use the local terminal")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,7 +183,7 @@ export class SessionTerminalManager {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
void vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", false)
|
||||
void this.host.setContext("kilo-code.agentTerminalFocus", false)
|
||||
for (const entry of this.terminals.values()) entry.terminal.dispose()
|
||||
this.terminals.clear()
|
||||
for (const d of this.commandDisposables.values()) d.dispose()
|
||||
@@ -171,27 +200,27 @@ export class SessionTerminalManager {
|
||||
}
|
||||
|
||||
this.commandHandlers.set(id, handler)
|
||||
this.commandDisposables.set(id, vscode.commands.registerCommand(id, handler))
|
||||
this.commandDisposables.set(id, this.host.registerCommand(id, handler))
|
||||
}
|
||||
|
||||
private async runOriginalCommand(id: string, args: unknown[]): Promise<unknown> {
|
||||
const disposable = this.commandDisposables.get(id)
|
||||
if (!disposable) return vscode.commands.executeCommand(id, ...args)
|
||||
if (!disposable) return this.host.executeCommand(id, ...args)
|
||||
|
||||
disposable.dispose()
|
||||
this.commandDisposables.delete(id)
|
||||
|
||||
try {
|
||||
return await vscode.commands.executeCommand(id, ...args)
|
||||
return await this.host.executeCommand(id, ...args)
|
||||
} finally {
|
||||
const handler = this.commandHandlers.get(id)
|
||||
if (!handler) return
|
||||
const replacement = vscode.commands.registerCommand(id, handler)
|
||||
const replacement = this.host.registerCommand(id, handler)
|
||||
this.commandDisposables.set(id, replacement)
|
||||
}
|
||||
}
|
||||
|
||||
private isManaged(terminal: vscode.Terminal): boolean {
|
||||
private isManaged(terminal: TerminalHandle): boolean {
|
||||
for (const entry of this.terminals.values()) {
|
||||
if (entry.terminal === terminal) return true
|
||||
}
|
||||
@@ -199,10 +228,10 @@ export class SessionTerminalManager {
|
||||
}
|
||||
|
||||
private updateContextKey(): void {
|
||||
const active = vscode.window.activeTerminal
|
||||
const active = this.host.activeTerminal()
|
||||
const managed = active ? this.isManaged(active) : false
|
||||
if (active) this.panelOpen = true
|
||||
void vscode.commands.executeCommand("setContext", "kilo-code.agentTerminalFocus", managed)
|
||||
void this.host.setContext("kilo-code.agentTerminalFocus", managed)
|
||||
}
|
||||
|
||||
private showOrCreate(sessionId: string, cwd: string, name: string): void {
|
||||
@@ -223,11 +252,7 @@ export class SessionTerminalManager {
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
const terminal = vscode.window.createTerminal({
|
||||
cwd,
|
||||
name,
|
||||
iconPath: new vscode.ThemeIcon("terminal"),
|
||||
})
|
||||
const terminal = this.host.createTerminal({ cwd, name })
|
||||
entry = { terminal, cwd }
|
||||
this.terminals.set(sessionId, entry)
|
||||
this.log(`showTerminal: created terminal for session ${sessionId} (cwd=${cwd})`)
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
/**
|
||||
* SetupScriptRunner - Executes worktree setup scripts
|
||||
*
|
||||
* Runs setup scripts as VS Code tasks before the agent starts.
|
||||
* This relies on VS Code's task execution model instead of manual terminal command strings.
|
||||
* Builds the platform-specific command for setup scripts and delegates
|
||||
* actual execution to an injected RunTask callback (provided by the caller).
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { SetupScriptService, type SetupScriptInfo } from "./SetupScriptService"
|
||||
|
||||
export interface SetupScriptEnvironment {
|
||||
interface SetupScriptEnvironment {
|
||||
/** Absolute path to the worktree directory */
|
||||
worktreePath: string
|
||||
/** Absolute path to the main repository */
|
||||
repoPath: string
|
||||
}
|
||||
|
||||
interface SetupTaskCommand {
|
||||
export interface SetupTaskConfig {
|
||||
command: string
|
||||
args: string[]
|
||||
cwd: string
|
||||
env: Record<string, string>
|
||||
}
|
||||
|
||||
const TASK_END_GRACE_MS = 250
|
||||
const TASK_TIMEOUT_MS = 5 * 60 * 1000
|
||||
/** Execute a task and return its exit code (undefined if unknown). */
|
||||
export type RunTask = (config: SetupTaskConfig) => Promise<number | undefined>
|
||||
|
||||
function quoteCmdArg(value: string): string {
|
||||
return `"${value.replaceAll('"', '""')}"`
|
||||
}
|
||||
|
||||
function buildSetupTaskCommand(script: SetupScriptInfo): SetupTaskCommand {
|
||||
export function buildSetupTaskCommand(script: SetupScriptInfo): { command: string; args: string[] } {
|
||||
if (script.kind === "powershell") {
|
||||
return {
|
||||
command: "powershell.exe",
|
||||
@@ -48,8 +49,9 @@ function buildSetupTaskCommand(script: SetupScriptInfo): SetupTaskCommand {
|
||||
|
||||
export class SetupScriptRunner {
|
||||
constructor(
|
||||
private readonly output: vscode.OutputChannel,
|
||||
private readonly log: (msg: string) => void,
|
||||
private readonly service: SetupScriptService,
|
||||
private readonly run: RunTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -68,7 +70,23 @@ export class SetupScriptRunner {
|
||||
this.log(`Running setup script: ${script.path}`)
|
||||
|
||||
try {
|
||||
await this.executeTask(script, env)
|
||||
const cmd = buildSetupTaskCommand(script)
|
||||
const code = await this.run({
|
||||
command: cmd.command,
|
||||
args: cmd.args,
|
||||
cwd: env.worktreePath,
|
||||
env: {
|
||||
WORKTREE_PATH: env.worktreePath,
|
||||
REPO_PATH: env.repoPath,
|
||||
},
|
||||
})
|
||||
if (code === undefined) {
|
||||
this.log("Setup script finished without a valid exit code — assuming success")
|
||||
return true
|
||||
}
|
||||
if (code !== 0) {
|
||||
throw new Error(`Setup script exited with code ${code}`)
|
||||
}
|
||||
this.log("Setup script completed")
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -77,101 +95,4 @@ export class SetupScriptRunner {
|
||||
return true // Script was attempted
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute setup script as a VS Code task and wait for completion. */
|
||||
private async executeTask(script: SetupScriptInfo, env: SetupScriptEnvironment): Promise<void> {
|
||||
const task = this.createTask(script, env)
|
||||
const execution = await vscode.tasks.executeTask(task)
|
||||
await this.waitForTaskEnd(execution)
|
||||
}
|
||||
|
||||
private createTask(script: SetupScriptInfo, env: SetupScriptEnvironment): vscode.Task {
|
||||
const cmd = buildSetupTaskCommand(script)
|
||||
const execution = new vscode.ProcessExecution(cmd.command, cmd.args, {
|
||||
cwd: env.worktreePath,
|
||||
env: {
|
||||
WORKTREE_PATH: env.worktreePath,
|
||||
REPO_PATH: env.repoPath,
|
||||
},
|
||||
})
|
||||
const task = new vscode.Task(
|
||||
{
|
||||
type: "kilo-worktree-setup",
|
||||
script: script.path,
|
||||
},
|
||||
vscode.TaskScope.Workspace,
|
||||
"Worktree Setup",
|
||||
"Kilo Code",
|
||||
execution,
|
||||
[],
|
||||
)
|
||||
task.presentationOptions = {
|
||||
reveal: vscode.TaskRevealKind.Always,
|
||||
panel: vscode.TaskPanelKind.Dedicated,
|
||||
clear: true,
|
||||
showReuseMessage: false,
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
private waitForTaskEnd(execution: vscode.TaskExecution): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = {
|
||||
done: false,
|
||||
grace: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
timeout: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
}
|
||||
|
||||
const finish = (error?: Error) => {
|
||||
if (state.done) return
|
||||
state.done = true
|
||||
if (state.grace) {
|
||||
clearTimeout(state.grace)
|
||||
}
|
||||
if (state.timeout) {
|
||||
clearTimeout(state.timeout)
|
||||
}
|
||||
processListener.dispose()
|
||||
endListener.dispose()
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
|
||||
const processListener = vscode.tasks.onDidEndTaskProcess((event) => {
|
||||
if (event.execution !== execution) return
|
||||
this.log(`Setup script exited with code ${event.exitCode ?? "unknown"}`)
|
||||
const code = event.exitCode
|
||||
if (typeof code !== "number") {
|
||||
finish(new Error("Setup script exited without a valid exit code"))
|
||||
return
|
||||
}
|
||||
if (code !== 0) {
|
||||
finish(new Error(`Setup script exited with code ${code}`))
|
||||
return
|
||||
}
|
||||
finish()
|
||||
})
|
||||
|
||||
const endListener = vscode.tasks.onDidEndTask((event) => {
|
||||
if (event.execution !== execution) return
|
||||
if (state.done) return
|
||||
state.grace = setTimeout(() => {
|
||||
this.log("Setup script finished without process exit event")
|
||||
finish()
|
||||
}, TASK_END_GRACE_MS)
|
||||
})
|
||||
|
||||
state.timeout = setTimeout(() => {
|
||||
this.log("Setup script timed out waiting for task completion")
|
||||
finish(new Error("Setup script timed out after 5 minutes"))
|
||||
}, TASK_TIMEOUT_MS)
|
||||
})
|
||||
}
|
||||
|
||||
private log(message: string): void {
|
||||
this.output.appendLine(`[SetupScriptRunner] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* Setup scripts run before an agent starts in a worktree (new sessions only).
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import { SETUP_SCRIPT_TEMPLATE, SETUP_SCRIPT_TEMPLATE_POWERSHELL } from "./setup-script-template"
|
||||
@@ -90,17 +89,6 @@ export class SetupScriptService {
|
||||
await fs.promises.writeFile(scriptPath, content, "utf-8")
|
||||
}
|
||||
|
||||
/** Open the setup script in VS Code editor. Creates the default script if it doesn't exist. */
|
||||
async openInEditor(platform: NodeJS.Platform = process.platform): Promise<void> {
|
||||
if (!this.hasScript(platform)) {
|
||||
await this.createDefaultScript(platform)
|
||||
}
|
||||
const resolved = this.resolveScript(platform)
|
||||
if (!resolved) return
|
||||
const document = await vscode.workspace.openTextDocument(resolved.path)
|
||||
await vscode.window.showTextDocument(document)
|
||||
}
|
||||
|
||||
private candidates(platform: NodeJS.Platform): SetupScriptCandidate[] {
|
||||
if (platform === "win32") {
|
||||
return [
|
||||
|
||||
@@ -26,41 +26,57 @@ import {
|
||||
type BranchListItem,
|
||||
} from "./git-import"
|
||||
|
||||
export type { BranchListItem }
|
||||
export { generateBranchName }
|
||||
|
||||
export interface WorktreeInfo {
|
||||
interface WorktreeInfo {
|
||||
branch: string
|
||||
path: string
|
||||
/** Bare branch name (e.g. "main"), without remote prefix. */
|
||||
parentBranch: string
|
||||
/** Remote name (e.g. "origin"). */
|
||||
remote?: string
|
||||
createdAt: number
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
export type StartPointSource = "remote" | "local-tracking" | "local-branch" | "fallback"
|
||||
|
||||
export interface StartPointResult {
|
||||
interface StartPointResult {
|
||||
ref: string
|
||||
/** Bare branch name (e.g. "main"), without remote prefix. */
|
||||
branch: string
|
||||
/** Remote name (e.g. "origin") when the start point came from a remote. */
|
||||
remote?: string
|
||||
source: StartPointSource
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type WorktreeProgressStep = "syncing" | "verifying" | "fetching" | "creating"
|
||||
type WorktreeProgressStep = "syncing" | "verifying" | "fetching" | "creating"
|
||||
|
||||
export interface CreateWorktreeResult {
|
||||
branch: string
|
||||
path: string
|
||||
/** Bare branch name (e.g. "main"), without remote prefix. */
|
||||
parentBranch: string
|
||||
/** Remote name (e.g. "origin"). */
|
||||
remote?: string
|
||||
startPointSource: StartPointSource
|
||||
startPointWarning?: string
|
||||
}
|
||||
|
||||
export interface ExternalWorktreeItem {
|
||||
interface ExternalWorktreeItem {
|
||||
path: string
|
||||
branch: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward compat: split a possibly-prefixed branch like "origin/main" into
|
||||
* `{ branch: "main", remote: "origin" }`. If no slash is found, returns bare branch.
|
||||
*/
|
||||
function stripRemotePrefix(ref: string): { branch: string; remote?: string } {
|
||||
const idx = ref.indexOf("/")
|
||||
if (idx > 0) return { branch: ref.slice(idx + 1), remote: ref.slice(0, idx) }
|
||||
return { branch: ref }
|
||||
}
|
||||
|
||||
const KILOCODE_DIR = ".kilocode"
|
||||
const SESSION_ID_FILE = "session-id"
|
||||
const METADATA_FILE = "metadata.json"
|
||||
@@ -140,16 +156,22 @@ export class WorktreeManager {
|
||||
await this.ensureDir()
|
||||
await this.ensureGitExclude()
|
||||
|
||||
// Resolve start point (parent branch)
|
||||
// Resolve start point (parent branch + remote)
|
||||
let parent: string
|
||||
let parentRemote: string | undefined
|
||||
let startPoint: StartPointResult | undefined
|
||||
|
||||
if (params.existingBranch) {
|
||||
// Existing branch provided directly
|
||||
// Existing branch provided directly — only attach remote when the
|
||||
// remote tracking ref actually exists (the branch may be local-only).
|
||||
const remote = await this.resolveRemote()
|
||||
const hasRemoteRef = remote && (await this.refExistsLocally(`${remote}/${params.existingBranch}`))
|
||||
parent = params.existingBranch
|
||||
parentRemote = hasRemoteRef ? remote : undefined
|
||||
startPoint = {
|
||||
ref: params.existingBranch,
|
||||
branch: params.existingBranch,
|
||||
remote: hasRemoteRef ? remote : undefined,
|
||||
source: "local-branch",
|
||||
}
|
||||
} else {
|
||||
@@ -161,6 +183,7 @@ export class WorktreeManager {
|
||||
allowFallback: !params.baseBranch, // Only fallback if user didn't explicitly request a specific base
|
||||
})
|
||||
parent = startPoint.branch
|
||||
parentRemote = startPoint.remote
|
||||
}
|
||||
|
||||
const sanitized = params.branchName ? sanitizeBranchName(params.branchName) : undefined
|
||||
@@ -210,11 +233,14 @@ export class WorktreeManager {
|
||||
await this.git.raw(retryArgs)
|
||||
}
|
||||
|
||||
this.log(`Created worktree: ${worktreePath} (branch: ${branch}, base: ${parent})`)
|
||||
this.log(
|
||||
`Created worktree: ${worktreePath} (branch: ${branch}, base: ${parentRemote ? `${parentRemote}/` : ""}${parent})`,
|
||||
)
|
||||
return {
|
||||
branch,
|
||||
path: worktreePath,
|
||||
parentBranch: parent,
|
||||
remote: parentRemote,
|
||||
startPointSource: startPoint.source,
|
||||
startPointWarning: startPoint.warning,
|
||||
}
|
||||
@@ -274,27 +300,38 @@ export class WorktreeManager {
|
||||
return results.filter((info): info is WorktreeInfo => info !== undefined)
|
||||
}
|
||||
|
||||
async writeMetadata(worktreePath: string, sessionId: string, parentBranch: string): Promise<void> {
|
||||
async writeMetadata(worktreePath: string, sessionId: string, parentBranch: string, remote?: string): Promise<void> {
|
||||
const dir = path.join(worktreePath, KILOCODE_DIR)
|
||||
if (!fs.existsSync(dir)) await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
// Write both formats: session-id for backward compat, metadata.json for parentBranch
|
||||
const meta: Record<string, string> = { sessionId, parentBranch }
|
||||
if (remote) meta.remote = remote
|
||||
|
||||
// Write both formats: session-id for backward compat, metadata.json for parentBranch+remote
|
||||
await Promise.all([
|
||||
fs.promises.writeFile(path.join(dir, SESSION_ID_FILE), sessionId, "utf-8"),
|
||||
fs.promises.writeFile(path.join(dir, METADATA_FILE), JSON.stringify({ sessionId, parentBranch }), "utf-8"),
|
||||
fs.promises.writeFile(path.join(dir, METADATA_FILE), JSON.stringify(meta), "utf-8"),
|
||||
])
|
||||
this.log(`Wrote metadata for session ${sessionId} to ${worktreePath}`)
|
||||
await this.ensureWorktreeExclude(worktreePath)
|
||||
}
|
||||
|
||||
async readMetadata(worktreePath: string): Promise<{ sessionId: string; parentBranch?: string } | undefined> {
|
||||
async readMetadata(
|
||||
worktreePath: string,
|
||||
): Promise<{ sessionId: string; parentBranch?: string; remote?: string } | undefined> {
|
||||
const dir = path.join(worktreePath, KILOCODE_DIR)
|
||||
|
||||
// Try metadata.json first (has parentBranch)
|
||||
// Try metadata.json first (has parentBranch + remote)
|
||||
try {
|
||||
const content = await fs.promises.readFile(path.join(dir, METADATA_FILE), "utf-8")
|
||||
const data = JSON.parse(content)
|
||||
if (data.sessionId) return { sessionId: data.sessionId, parentBranch: data.parentBranch }
|
||||
if (data.sessionId) {
|
||||
return {
|
||||
sessionId: data.sessionId,
|
||||
parentBranch: data.parentBranch,
|
||||
remote: data.remote,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall back to session-id file
|
||||
}
|
||||
@@ -413,12 +450,27 @@ export class WorktreeManager {
|
||||
fs.promises.stat(wtPath),
|
||||
this.readMetadata(wtPath),
|
||||
])
|
||||
// Use persisted parentBranch if available, fall back to defaultBranch
|
||||
const parent = meta?.parentBranch ?? (await this.defaultBranch())
|
||||
// Use persisted metadata if available, fall back to resolveBaseBranch.
|
||||
// Backward compat: old metadata may store "origin/main" in parentBranch without
|
||||
// a separate remote field. Try to detect this by checking if the prefix is a known remote.
|
||||
const base =
|
||||
(await (async () => {
|
||||
if (!meta?.parentBranch) return undefined
|
||||
if (meta.remote) return { branch: meta.parentBranch, remote: meta.remote }
|
||||
// Backward compat: old metadata stored "origin/main" in parentBranch.
|
||||
// Only split when the prefix is a known remote name (not a branch like "release/1.0").
|
||||
const split = stripRemotePrefix(meta.parentBranch)
|
||||
if (split.remote) {
|
||||
const remotes = await this.git.getRemotes().catch(() => [])
|
||||
if (remotes.some((r) => r.name === split.remote)) return split
|
||||
}
|
||||
return { branch: meta.parentBranch }
|
||||
})()) ?? (await this.resolveBaseBranch())
|
||||
return {
|
||||
branch: branch.trim(),
|
||||
path: wtPath,
|
||||
parentBranch: parent,
|
||||
parentBranch: base.branch,
|
||||
remote: base.remote,
|
||||
createdAt: stat.birthtimeMs,
|
||||
sessionId: meta?.sessionId,
|
||||
}
|
||||
@@ -436,27 +488,30 @@ export class WorktreeManager {
|
||||
const { allowFallback = true } = opts || {}
|
||||
|
||||
// 1. Remote fetch
|
||||
if (await this.hasOriginRemote()) {
|
||||
onProgress?.("fetching", `Fetching origin/${branch}...`)
|
||||
const remote = await this.resolveRemote()
|
||||
if (remote) {
|
||||
onProgress?.("fetching", `Fetching ${remote}/${branch}...`)
|
||||
try {
|
||||
await this.git.fetch("origin", branch)
|
||||
if (await this.refExistsLocally(`origin/${branch}`)) {
|
||||
await this.git.fetch(remote, branch)
|
||||
if (await this.refExistsLocally(`${remote}/${branch}`)) {
|
||||
return {
|
||||
ref: `origin/${branch}`,
|
||||
branch: branch,
|
||||
ref: `${remote}/${branch}`,
|
||||
branch,
|
||||
remote,
|
||||
source: "remote",
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(`Failed to fetch origin/${branch}: ${e}`)
|
||||
this.log(`Failed to fetch ${remote}/${branch}: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Stale local tracking ref (offline fallback)
|
||||
if (await this.refExistsLocally(`origin/${branch}`)) {
|
||||
if (remote && (await this.refExistsLocally(`${remote}/${branch}`))) {
|
||||
return {
|
||||
ref: `origin/${branch}`,
|
||||
branch: branch,
|
||||
ref: `${remote}/${branch}`,
|
||||
branch,
|
||||
remote,
|
||||
source: "local-tracking",
|
||||
warning: "Used stale remote tracking branch (fetch failed)",
|
||||
}
|
||||
@@ -466,7 +521,7 @@ export class WorktreeManager {
|
||||
if (await this.refExistsLocally(branch)) {
|
||||
return {
|
||||
ref: branch,
|
||||
branch: branch,
|
||||
branch,
|
||||
source: "local-branch",
|
||||
}
|
||||
}
|
||||
@@ -492,13 +547,23 @@ export class WorktreeManager {
|
||||
throw new Error(`Could not resolve start point for branch "${branch}"`)
|
||||
}
|
||||
|
||||
async hasOriginRemote(): Promise<boolean> {
|
||||
try {
|
||||
const remotes = await this.git.getRemotes()
|
||||
return remotes.some((r) => r.name === "origin")
|
||||
} catch {
|
||||
return false
|
||||
/**
|
||||
* Resolve the primary remote name for this repo.
|
||||
* Uses `GitOps.resolveRemote` when available, otherwise checks for "origin".
|
||||
* Returns `undefined` when no remote exists (local-only repo).
|
||||
*/
|
||||
async resolveRemote(): Promise<string | undefined> {
|
||||
if (this.ops) {
|
||||
const name = await this.ops.resolveRemote(this.root).catch(() => "origin")
|
||||
const remotes = await this.git.getRemotes().catch(() => [])
|
||||
return remotes.some((r) => r.name === name) ? name : undefined
|
||||
}
|
||||
const remotes = await this.git.getRemotes().catch(() => [])
|
||||
return remotes.some((r) => r.name === "origin") ? "origin" : undefined
|
||||
}
|
||||
|
||||
async hasOriginRemote(): Promise<boolean> {
|
||||
return (await this.resolveRemote()) !== undefined
|
||||
}
|
||||
|
||||
async refExistsLocally(ref: string): Promise<boolean> {
|
||||
@@ -565,14 +630,32 @@ export class WorktreeManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the base branch and remote for diffs and comparisons.
|
||||
* Returns a bare branch name + remote name so callers can construct
|
||||
* `${remote}/${branch}` at diff time (mirroring what a PR would show).
|
||||
*/
|
||||
async resolveBaseBranch(): Promise<{ branch: string; remote?: string }> {
|
||||
const branch = await this.defaultBranch()
|
||||
const remote = await this.resolveRemote()
|
||||
if (remote && (await this.refExistsLocally(`${remote}/${branch}`))) {
|
||||
return { branch, remote }
|
||||
}
|
||||
return { branch }
|
||||
}
|
||||
|
||||
async defaultBranch(): Promise<string> {
|
||||
// 1. Try symbolic-ref
|
||||
try {
|
||||
const head = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"])
|
||||
const match = head.trim().match(/refs\/remotes\/origin\/(.+)$/)
|
||||
if (match) return match[1]
|
||||
} catch (e) {
|
||||
this.log(`defaultBranch: symbolic-ref failed: ${e}`)
|
||||
// 1. Try symbolic-ref against the resolved remote (not hardcoded "origin")
|
||||
const remote = await this.resolveRemote()
|
||||
if (remote) {
|
||||
try {
|
||||
const head = await this.git.raw(["symbolic-ref", `refs/remotes/${remote}/HEAD`])
|
||||
const prefix = `refs/remotes/${remote}/`
|
||||
const trimmed = head.trim()
|
||||
if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length)
|
||||
} catch (e) {
|
||||
this.log(`defaultBranch: symbolic-ref for ${remote} failed: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try current branch (if not detached)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* (many sessions per worktree) and provides CRUD operations for both.
|
||||
*
|
||||
* Data model:
|
||||
* - Worktree: a git worktree with branch, path, parentBranch
|
||||
* - Worktree: a git worktree with branch, path, parentBranch (bare), remote
|
||||
* - ManagedSession: a server session ID associated with a worktree (or null for local)
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,10 @@ export interface Worktree {
|
||||
id: string
|
||||
branch: string
|
||||
path: string
|
||||
/** Bare branch name (e.g. "main"), without remote prefix. */
|
||||
parentBranch: string
|
||||
/** Remote name (e.g. "origin"). When set, diffs compare against `${remote}/${parentBranch}`. */
|
||||
remote?: string
|
||||
createdAt: string
|
||||
/** Shared identifier for worktrees created together via multi-version mode. */
|
||||
groupId?: string
|
||||
@@ -25,7 +28,16 @@ export interface Worktree {
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface ManagedSession {
|
||||
/**
|
||||
* Construct the remote-prefixed ref for diff comparisons.
|
||||
* Returns `${remote}/${branch}` when a remote is known, otherwise the bare branch.
|
||||
* This mirrors Superset's pattern of always diffing against the remote tracking ref.
|
||||
*/
|
||||
export function remoteRef(wt: Pick<Worktree, "parentBranch" | "remote">): string {
|
||||
return wt.remote ? `${wt.remote}/${wt.parentBranch}` : wt.parentBranch
|
||||
}
|
||||
|
||||
interface ManagedSession {
|
||||
id: string
|
||||
worktreeId: string | null
|
||||
createdAt: string
|
||||
@@ -121,6 +133,7 @@ export class WorktreeStateManager {
|
||||
branch: string
|
||||
path: string
|
||||
parentBranch: string
|
||||
remote?: string
|
||||
groupId?: string
|
||||
label?: string
|
||||
}): Worktree {
|
||||
@@ -132,6 +145,7 @@ export class WorktreeStateManager {
|
||||
parentBranch: params.parentBranch,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
if (params.remote) wt.remote = params.remote
|
||||
if (params.groupId) wt.groupId = params.groupId
|
||||
if (params.label) wt.label = params.label
|
||||
this.worktrees.set(id, wt)
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import * as fs from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
openTextDocument: vi.fn(),
|
||||
},
|
||||
window: {
|
||||
showTextDocument: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import { SetupScriptService } from "../SetupScriptService"
|
||||
|
||||
function setupRoot(): string {
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface BranchListItem {
|
||||
isCheckedOut?: boolean
|
||||
}
|
||||
|
||||
export interface PRUrlParts {
|
||||
interface PRUrlParts {
|
||||
owner: string
|
||||
repo: string
|
||||
number: number
|
||||
@@ -20,14 +20,14 @@ export interface PRInfo {
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface WorktreeEntry {
|
||||
interface WorktreeEntry {
|
||||
path: string
|
||||
branch: string
|
||||
bare: boolean
|
||||
detached: boolean
|
||||
}
|
||||
|
||||
export type PRErrorKind = "not_found" | "gh_missing" | "gh_auth" | "unknown"
|
||||
type PRErrorKind = "not_found" | "gh_missing" | "gh_auth" | "unknown"
|
||||
|
||||
export function parsePRUrl(url: string): PRUrlParts | null {
|
||||
let normalized = url.trim()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* VS Code adapter implementing the RunTask callback via vscode.tasks API.
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import type { SetupTaskConfig } from "./SetupScriptRunner"
|
||||
|
||||
const GRACE_MS = 250
|
||||
const TIMEOUT_MS = 5 * 60 * 1000
|
||||
|
||||
export async function executeVscodeTask(config: SetupTaskConfig): Promise<number | undefined> {
|
||||
const proc = new vscode.ProcessExecution(config.command, config.args, {
|
||||
cwd: config.cwd,
|
||||
env: config.env,
|
||||
})
|
||||
const task = new vscode.Task(
|
||||
{ type: "kilo-worktree-setup", script: config.command },
|
||||
vscode.TaskScope.Workspace,
|
||||
"Worktree Setup",
|
||||
"Kilo Code",
|
||||
proc,
|
||||
[],
|
||||
)
|
||||
task.presentationOptions = {
|
||||
reveal: vscode.TaskRevealKind.Always,
|
||||
panel: vscode.TaskPanelKind.Dedicated,
|
||||
clear: true,
|
||||
showReuseMessage: false,
|
||||
}
|
||||
|
||||
const execution = await vscode.tasks.executeTask(task)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false
|
||||
let grace: ReturnType<typeof setTimeout> | undefined
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const finish = (code?: number, error?: Error) => {
|
||||
if (done) return
|
||||
done = true
|
||||
if (grace) clearTimeout(grace)
|
||||
if (timeout) clearTimeout(timeout)
|
||||
processListener.dispose()
|
||||
endListener.dispose()
|
||||
if (error) reject(error)
|
||||
else resolve(code)
|
||||
}
|
||||
|
||||
const processListener = vscode.tasks.onDidEndTaskProcess((event) => {
|
||||
if (event.execution !== execution) return
|
||||
finish(event.exitCode ?? undefined)
|
||||
})
|
||||
|
||||
const endListener = vscode.tasks.onDidEndTask((event) => {
|
||||
if (event.execution !== execution) return
|
||||
if (done) return
|
||||
grace = setTimeout(() => finish(undefined), GRACE_MS)
|
||||
})
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
finish(undefined, new Error("Setup script timed out after 5 minutes"))
|
||||
}, TIMEOUT_MS)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* VS Code adapter implementing the TerminalHost interface.
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import type { TerminalHost, TerminalHandle } from "./SessionTerminalManager"
|
||||
|
||||
export function createTerminalHost(): TerminalHost {
|
||||
const terminalMap = new WeakMap<vscode.Terminal, TerminalHandle>()
|
||||
|
||||
const wrap = (terminal: vscode.Terminal): TerminalHandle => {
|
||||
const existing = terminalMap.get(terminal)
|
||||
if (existing) return existing
|
||||
const handle: TerminalHandle = {
|
||||
show: (preserveFocus) => terminal.show(preserveFocus),
|
||||
dispose: () => terminal.dispose(),
|
||||
get exitStatus() {
|
||||
return terminal.exitStatus ? { code: terminal.exitStatus.code } : undefined
|
||||
},
|
||||
}
|
||||
terminalMap.set(terminal, handle)
|
||||
return handle
|
||||
}
|
||||
|
||||
return {
|
||||
createTerminal: (opts) =>
|
||||
wrap(
|
||||
vscode.window.createTerminal({
|
||||
cwd: opts.cwd,
|
||||
name: opts.name,
|
||||
iconPath: new vscode.ThemeIcon("terminal"),
|
||||
}),
|
||||
),
|
||||
activeTerminal: () => {
|
||||
const t = vscode.window.activeTerminal
|
||||
return t ? wrap(t) : undefined
|
||||
},
|
||||
workspacePath: () => vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
|
||||
showWarning: (msg) => void vscode.window.showWarningMessage(msg),
|
||||
setContext: (key, value) => void vscode.commands.executeCommand("setContext", key, value),
|
||||
onTerminalClosed: (cb) => vscode.window.onDidCloseTerminal((terminal) => cb(wrap(terminal))),
|
||||
onActiveTerminalChanged: (cb) =>
|
||||
vscode.window.onDidChangeActiveTerminal((terminal) => cb(terminal ? wrap(terminal) : undefined)),
|
||||
registerCommand: (id, handler) => vscode.commands.registerCommand(id, handler),
|
||||
executeCommand: (id, ...args) => Promise.resolve(vscode.commands.executeCommand(id, ...args)),
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
* - contributes.viewsContainers.activitybar[0].title
|
||||
* - contributes.views.kilo-code-sidebar[0].name
|
||||
*/
|
||||
export const NEW_EXTENSION_IS_STILL_EXPERIMENTAL_SO_SHOW_EXTRA_TEXTS_TO_SHOW_DIFFERENCE = true
|
||||
const NEW_EXTENSION_IS_STILL_EXPERIMENTAL_SO_SHOW_EXTRA_TEXTS_TO_SHOW_DIFFERENCE = true
|
||||
|
||||
export const EXTENSION_DISPLAY_NAME =
|
||||
"Kilo Code" + (NEW_EXTENSION_IS_STILL_EXPERIMENTAL_SO_SHOW_EXTRA_TEXTS_TO_SHOW_DIFFERENCE ? " (NEW)" : "")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as vscode from "vscode"
|
||||
import { KiloProvider } from "./KiloProvider"
|
||||
import { AgentManagerProvider } from "./agent-manager/AgentManagerProvider"
|
||||
import { DiffViewerProvider } from "./DiffViewerProvider"
|
||||
import { SettingsEditorProvider } from "./SettingsEditorProvider"
|
||||
import { EXTENSION_DISPLAY_NAME } from "./constants"
|
||||
import { KiloConnectionService } from "./services/cli-backend"
|
||||
import { registerAutocompleteProvider } from "./services/autocomplete"
|
||||
@@ -47,6 +49,17 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
const agentManagerProvider = new AgentManagerProvider(context.extensionUri, connectionService)
|
||||
context.subscriptions.push(agentManagerProvider)
|
||||
|
||||
// Create standalone diff viewer provider for the sidebar "Show Changes" action
|
||||
const diffViewerProvider = new DiffViewerProvider(context.extensionUri, connectionService)
|
||||
diffViewerProvider.setCommentHandler((comments) => {
|
||||
void provider.appendReviewComments(comments)
|
||||
})
|
||||
context.subscriptions.push(diffViewerProvider)
|
||||
|
||||
// Create settings/profile editor provider (opens in editor area, not sidebar)
|
||||
const settingsEditorProvider = new SettingsEditorProvider(context.extensionUri, connectionService, context)
|
||||
context.subscriptions.push(settingsEditorProvider)
|
||||
|
||||
// Register toolbar button command handlers
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => {
|
||||
@@ -65,10 +78,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
provider.postMessage({ type: "action", action: "cloudHistoryButtonClicked" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.profileButtonClicked", () => {
|
||||
provider.postMessage({ type: "action", action: "profileButtonClicked" })
|
||||
settingsEditorProvider.openPanel("profile")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.settingsButtonClicked", () => {
|
||||
provider.postMessage({ type: "action", action: "settingsButtonClicked" })
|
||||
settingsEditorProvider.openPanel("settings")
|
||||
}),
|
||||
// legacy-migration start
|
||||
vscode.commands.registerCommand("kilo-code.new.openMigrationWizard", () => {
|
||||
@@ -78,6 +91,9 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand("kilo-code.new.openInTab", () => {
|
||||
return openKiloInNewTab(context, connectionService)
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.showChanges", () => {
|
||||
diffViewerProvider.openPanel()
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.previousSession", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "sessionPrevious" })
|
||||
}),
|
||||
|
||||
@@ -169,6 +169,8 @@ export type WebviewMessage =
|
||||
| { type: "todoUpdated"; sessionID: string; items: unknown[] }
|
||||
| { type: "questionRequest"; question: { id: string; sessionID: string; questions: unknown[]; tool?: unknown } }
|
||||
| { type: "questionResolved"; requestID: string }
|
||||
| { type: "permissionResolved"; permissionID: string }
|
||||
| { type: "permissionError"; permissionID: string }
|
||||
| { type: "sessionCreated"; session: ReturnType<typeof sessionToWebview> }
|
||||
| { type: "sessionUpdated"; session: ReturnType<typeof sessionToWebview> }
|
||||
| null
|
||||
@@ -228,6 +230,11 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
|
||||
tool: event.properties.tool,
|
||||
},
|
||||
}
|
||||
case "permission.replied":
|
||||
return {
|
||||
type: "permissionResolved",
|
||||
permissionID: event.properties.requestID,
|
||||
}
|
||||
case "todo.updated":
|
||||
return {
|
||||
type: "todoUpdated",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* legacy-migration - Barrel export.
|
||||
* Delete this entire directory when dropping legacy migration support.
|
||||
*/
|
||||
export * from "./legacy-types"
|
||||
export * from "./provider-mapping"
|
||||
export * from "./migration-messages"
|
||||
export * from "./migration-service"
|
||||
@@ -229,7 +229,7 @@ export interface LegacySettings {
|
||||
// Custom modes (stored on disk at <globalStorage>/settings/custom_modes.yaml)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface LegacyCustomModesFile {
|
||||
interface LegacyCustomModesFile {
|
||||
customModes: LegacyCustomMode[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* legacy-migration - Message type definitions for migration wizard communication.
|
||||
*
|
||||
* These types extend the extension ↔ webview message contract specifically for
|
||||
* the legacy migration wizard. They are defined here to keep them isolated from
|
||||
* the main messages.ts file and easy to remove.
|
||||
*/
|
||||
|
||||
import type {
|
||||
MigrationProviderInfo,
|
||||
MigrationMcpServerInfo,
|
||||
MigrationCustomModeInfo,
|
||||
MigrationSelections,
|
||||
MigrationResultItem,
|
||||
LegacySettings,
|
||||
} from "./legacy-types"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension → Webview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Sends detected legacy data to the wizard for display in the selection step. */
|
||||
export interface LegacyMigrationDataMessage {
|
||||
type: "legacyMigrationData"
|
||||
data: {
|
||||
providers: MigrationProviderInfo[]
|
||||
mcpServers: MigrationMcpServerInfo[]
|
||||
customModes: MigrationCustomModeInfo[]
|
||||
defaultModel?: { provider: string; model: string }
|
||||
settings?: LegacySettings
|
||||
}
|
||||
}
|
||||
|
||||
/** Real-time progress update for a single item being migrated. */
|
||||
export interface LegacyMigrationProgressMessage {
|
||||
type: "legacyMigrationProgress"
|
||||
item: string
|
||||
status: "migrating" | "success" | "warning" | "error"
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** Final results once all selected items have been processed. */
|
||||
export interface LegacyMigrationCompleteMessage {
|
||||
type: "legacyMigrationComplete"
|
||||
results: MigrationResultItem[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webview → Extension
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Webview requests the legacy data payload (e.g. on component mount). */
|
||||
export interface RequestLegacyMigrationDataMessage {
|
||||
type: "requestLegacyMigrationData"
|
||||
}
|
||||
|
||||
/** User has confirmed selections and wants to start migration. */
|
||||
export interface StartLegacyMigrationMessage {
|
||||
type: "startLegacyMigration"
|
||||
selections: MigrationSelections
|
||||
}
|
||||
|
||||
/** User chose to skip migration entirely. */
|
||||
export interface SkipLegacyMigrationMessage {
|
||||
type: "skipLegacyMigration"
|
||||
}
|
||||
|
||||
/** User opted to clear legacy data after successful migration. */
|
||||
export interface ClearLegacyDataMessage {
|
||||
type: "clearLegacyData"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { inspect } from "util"
|
||||
import type { FileDiff } from "@kilocode/sdk/v2/client"
|
||||
import { GitOps } from "./agent-manager/GitOps"
|
||||
|
||||
export function appendOutput(channel: vscode.OutputChannel, prefix: string, ...args: unknown[]): void {
|
||||
const msg = args
|
||||
.map((item) => (typeof item === "string" ? item : inspect(item, { breakLength: Infinity, depth: 4 })))
|
||||
.join(" ")
|
||||
channel.appendLine(`[${prefix}] ${msg}`)
|
||||
}
|
||||
|
||||
export function getWorkspaceRoot(): string | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders
|
||||
if (folders && folders.length > 0) return folders[0].uri.fsPath
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function resolveLocalDiffTarget(
|
||||
gitOps: GitOps,
|
||||
log: (...args: unknown[]) => void,
|
||||
): Promise<{ directory: string; baseBranch: string } | undefined> {
|
||||
const root = getWorkspaceRoot()
|
||||
if (!root) {
|
||||
log("Local diff: no workspace root")
|
||||
return
|
||||
}
|
||||
|
||||
const branch = await gitOps.currentBranch(root)
|
||||
if (!branch || branch === "HEAD") {
|
||||
log("Local diff: detached HEAD or no branch")
|
||||
return
|
||||
}
|
||||
|
||||
const tracking = await gitOps.resolveTrackingBranch(root, branch)
|
||||
const fallback = tracking ? undefined : await gitOps.resolveDefaultBranch(root, branch)
|
||||
const base = tracking ?? fallback ?? "HEAD"
|
||||
|
||||
log(`Local diff: branch=${branch} tracking=${tracking ?? "none"} default=${fallback ?? "none"} base=${base}`)
|
||||
|
||||
return { directory: root, baseBranch: base }
|
||||
}
|
||||
|
||||
export function hashFileDiffs(diffs: FileDiff[]): string {
|
||||
return diffs.map((diff) => `${diff.file}:${diff.status}:${diff.additions}:${diff.deletions}:${diff.after}`).join("|")
|
||||
}
|
||||
|
||||
export function openFileInEditor(
|
||||
filePath: string,
|
||||
line?: number,
|
||||
column?: number,
|
||||
viewColumn: vscode.ViewColumn = vscode.ViewColumn.Beside,
|
||||
prefix = "Kilo",
|
||||
): void {
|
||||
const uri = vscode.Uri.file(filePath)
|
||||
const target = Math.max(1, Math.floor(line ?? 1))
|
||||
const col = column !== undefined && column > 0 ? column - 1 : 0
|
||||
const pos = new vscode.Position(target - 1, col)
|
||||
const selection = new vscode.Range(pos, pos)
|
||||
|
||||
vscode.workspace.openTextDocument(uri).then(
|
||||
(doc) => vscode.window.showTextDocument(doc, { viewColumn, preview: true, selection }),
|
||||
(err) => console.error(`[Kilo New] ${prefix}: Failed to open file:`, uri.fsPath, err),
|
||||
)
|
||||
}
|
||||
|
||||
export function openWorkspaceRelativeFile(relativePath: string, line?: number, column?: number): void {
|
||||
const root = getWorkspaceRoot()
|
||||
if (!root) return
|
||||
const resolved = path.resolve(root, relativePath)
|
||||
if (!resolved.startsWith(root + path.sep) && resolved !== root) return
|
||||
openFileInEditor(resolved, line, column, vscode.ViewColumn.Beside, "DiffViewerProvider")
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export { BrowserAutomationService, type BrowserAutomationState } from "./browser-automation-service"
|
||||
export { BrowserAutomationService } from "./browser-automation-service"
|
||||
|
||||
@@ -1,38 +1,5 @@
|
||||
// Main exports for cli-backend services
|
||||
|
||||
// SDK types — re-exported so consumers can import from "cli-backend" barrel
|
||||
export type {
|
||||
Session,
|
||||
SessionStatus,
|
||||
Message as MessageInfo,
|
||||
Part as MessagePart,
|
||||
ToolState,
|
||||
PermissionRequest,
|
||||
Event,
|
||||
Todo,
|
||||
Agent,
|
||||
Provider,
|
||||
Model as ProviderModel,
|
||||
McpStatus,
|
||||
McpLocalConfig,
|
||||
McpRemoteConfig,
|
||||
Config,
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
|
||||
// Local types — extension-specific, not from the API
|
||||
export type {
|
||||
ServerConfig,
|
||||
EditorContext,
|
||||
KilocodeNotification,
|
||||
KilocodeNotificationAction,
|
||||
CloudSessionData,
|
||||
} from "./types"
|
||||
|
||||
export { ServerManager } from "./server-manager"
|
||||
export type { ServerInstance } from "./server-manager"
|
||||
|
||||
export { SdkSSEAdapter } from "./sdk-sse-adapter"
|
||||
export type { SSEEventHandler, SSEErrorHandler, SSEStateHandler } from "./sdk-sse-adapter"
|
||||
export type { KilocodeNotification } from "./types"
|
||||
|
||||
export { KiloConnectionService } from "./connection-service"
|
||||
export type { ConnectionState } from "./connection-service"
|
||||
|
||||
@@ -79,6 +79,7 @@ export class ServerManager {
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
windowsHide: true,
|
||||
})
|
||||
console.log("[Kilo New] ServerManager: 📦 Process spawned with PID:", serverProcess.pid)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ServerConfig {
|
||||
}
|
||||
|
||||
// Provider OAuth types
|
||||
export interface ProviderAuthAuthorization {
|
||||
interface ProviderAuthAuthorization {
|
||||
url: string
|
||||
method: "auto" | "code"
|
||||
instructions: string
|
||||
@@ -49,14 +49,14 @@ export interface KilocodeBalance {
|
||||
balance: number
|
||||
}
|
||||
|
||||
export interface ProfileData {
|
||||
interface ProfileData {
|
||||
profile: KilocodeProfile
|
||||
balance: KilocodeBalance | null
|
||||
currentOrgId: string | null
|
||||
}
|
||||
|
||||
// Cloud session from the Kilo cloud API (cli_sessions_v2)
|
||||
export interface CloudSessionInfo {
|
||||
interface CloudSessionInfo {
|
||||
session_id: string
|
||||
title: string | null
|
||||
created_at: string
|
||||
@@ -93,7 +93,7 @@ export interface CloudSessionData {
|
||||
}
|
||||
|
||||
/** VS Code editor context sent alongside messages to the CLI backend */
|
||||
export interface WorktreeFileDiff {
|
||||
interface WorktreeFileDiff {
|
||||
file: string
|
||||
before: string
|
||||
after: string
|
||||
|
||||
@@ -1,11 +1,2 @@
|
||||
export { TelemetryEventName, type TelemetryPropertiesProvider } from "./types"
|
||||
export {
|
||||
ApiProviderError,
|
||||
isApiProviderError,
|
||||
getApiProviderErrorProperties,
|
||||
ConsecutiveMistakeError,
|
||||
isConsecutiveMistakeError,
|
||||
getConsecutiveMistakeErrorProperties,
|
||||
type ConsecutiveMistakeReason,
|
||||
} from "./errors"
|
||||
export { TelemetryProxy } from "./telemetry-proxy"
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as crypto from "crypto"
|
||||
import * as vscode from "vscode"
|
||||
import { buildCspString } from "./webview-html-utils"
|
||||
|
||||
export function getNonce(): string {
|
||||
function getNonce(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function buildConnectSrc(port?: number): string {
|
||||
/**
|
||||
* Join an array of CSP directives into a policy string.
|
||||
*/
|
||||
export function joinCspDirectives(directives: string[]): string {
|
||||
function joinCspDirectives(directives: string[]): string {
|
||||
return directives.join("; ")
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,13 @@ import { Project, SyntaxKind } from "ts-morph"
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "../..")
|
||||
const KILO_PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
|
||||
const CSS_FILE = path.join(ROOT, "webview-ui/agent-manager/agent-manager.css")
|
||||
const CSS_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/agent-manager.css"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/agent-manager-review.css"),
|
||||
]
|
||||
const TSX_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/FullScreenDiffView.tsx"),
|
||||
@@ -32,13 +36,17 @@ const TSX_FILE = TSX_FILES[0]
|
||||
const PROVIDER_FILE = path.join(ROOT, "src/agent-manager/AgentManagerProvider.ts")
|
||||
const SETUP_SCRIPT_RUNNER_FILE = path.join(ROOT, "src/agent-manager/SetupScriptRunner.ts")
|
||||
|
||||
function readAllCss(): string {
|
||||
return CSS_FILES.map((f) => fs.readFileSync(f, "utf-8")).join("\n")
|
||||
}
|
||||
|
||||
function readAllTsx(): string {
|
||||
return TSX_FILES.map((f) => fs.readFileSync(f, "utf-8")).join("\n")
|
||||
}
|
||||
|
||||
describe("Agent Manager CSS Prefix", () => {
|
||||
it("all class selectors should use am- prefix", () => {
|
||||
const css = fs.readFileSync(CSS_FILE, "utf-8")
|
||||
const css = readAllCss()
|
||||
const matches = [...css.matchAll(/\.([a-z][a-z0-9-]*)/gi)]
|
||||
const names = [...new Set(matches.map((m) => m[1]))]
|
||||
|
||||
@@ -48,7 +56,7 @@ describe("Agent Manager CSS Prefix", () => {
|
||||
})
|
||||
|
||||
it("all CSS custom properties should use am- prefix", () => {
|
||||
const css = fs.readFileSync(CSS_FILE, "utf-8")
|
||||
const css = readAllCss()
|
||||
const matches = [...css.matchAll(/--([a-z][a-z0-9-]*)\s*:/gi)]
|
||||
const names = [...new Set(matches.map((m) => m[1]))]
|
||||
|
||||
@@ -61,7 +69,7 @@ describe("Agent Manager CSS Prefix", () => {
|
||||
})
|
||||
|
||||
it("all @keyframes should use am- prefix", () => {
|
||||
const css = fs.readFileSync(CSS_FILE, "utf-8")
|
||||
const css = readAllCss()
|
||||
const matches = [...css.matchAll(/@keyframes\s+([a-z][a-z0-9-]*)/gi)]
|
||||
const names = matches.map((m) => m[1])
|
||||
|
||||
@@ -73,7 +81,7 @@ describe("Agent Manager CSS Prefix", () => {
|
||||
|
||||
describe("Agent Manager CSS/TSX Consistency", () => {
|
||||
it("all classes used in TSX should be defined in CSS", () => {
|
||||
const css = fs.readFileSync(CSS_FILE, "utf-8")
|
||||
const css = readAllCss()
|
||||
const tsx = readAllTsx()
|
||||
|
||||
// Extract am- classes defined in CSS
|
||||
@@ -90,7 +98,7 @@ describe("Agent Manager CSS/TSX Consistency", () => {
|
||||
})
|
||||
|
||||
it("all am- classes defined in CSS should be used in TSX", () => {
|
||||
const css = fs.readFileSync(CSS_FILE, "utf-8")
|
||||
const css = readAllCss()
|
||||
const tsx = readAllTsx()
|
||||
|
||||
// Extract am- classes defined in CSS
|
||||
@@ -434,22 +442,137 @@ describe("Agent Manager — dialog listener cleanup", () => {
|
||||
|
||||
describe("SetupScriptRunner — task execution model", () => {
|
||||
const runner = fs.readFileSync(SETUP_SCRIPT_RUNNER_FILE, "utf-8")
|
||||
const taskAdapter = fs.readFileSync(path.join(ROOT, "src/agent-manager/task-runner.ts"), "utf-8")
|
||||
|
||||
it("uses VS Code tasks API for setup execution", () => {
|
||||
expect(runner).toContain("vscode.tasks.executeTask")
|
||||
expect(runner).toContain("onDidEndTaskProcess")
|
||||
expect(runner).toContain("onDidEndTask")
|
||||
it("runner is vscode-free and delegates execution via RunTask callback", () => {
|
||||
expect(runner).not.toContain("vscode")
|
||||
expect(runner).toContain("RunTask")
|
||||
expect(runner).toContain("buildSetupTaskCommand")
|
||||
})
|
||||
|
||||
it("uses process-based task execution with env options", () => {
|
||||
expect(runner).toContain("new vscode.ProcessExecution")
|
||||
it("runner still provides WORKTREE_PATH and REPO_PATH env vars", () => {
|
||||
expect(runner).toContain("WORKTREE_PATH")
|
||||
expect(runner).toContain("REPO_PATH")
|
||||
})
|
||||
|
||||
it("task-runner adapter hosts the vscode task execution", () => {
|
||||
expect(taskAdapter).toContain("vscode.tasks.executeTask")
|
||||
expect(taskAdapter).toContain("onDidEndTaskProcess")
|
||||
expect(taskAdapter).toContain("new vscode.ProcessExecution")
|
||||
})
|
||||
|
||||
it("does not use manual terminal command injection", () => {
|
||||
expect(runner).not.toContain("createTerminal")
|
||||
expect(runner).not.toContain("sendText")
|
||||
expect(runner).not.toContain("buildSetupCommand")
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VS Code import boundary — layering enforcement
|
||||
//
|
||||
// The agent-manager is being decoupled from VS Code so it can eventually run
|
||||
// outside the extension host. These tests enforce the layering:
|
||||
//
|
||||
// 1. Only files on the VSCODE_ALLOWED list may import "vscode".
|
||||
// 2. Each allowed file has a maxLines cap — shrink it as logic is extracted.
|
||||
//
|
||||
// To improve the architecture:
|
||||
// - Extract business logic from allowed files into vscode-free modules.
|
||||
// - Lower maxLines once the extraction lands.
|
||||
// - Remove entries from VSCODE_ALLOWED once they no longer need vscode.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AGENT_MANAGER_DIR = path.join(ROOT, "src/agent-manager")
|
||||
|
||||
/**
|
||||
* Exception list: files currently allowed to import `vscode`.
|
||||
*
|
||||
* Each entry has a maxLines cap. The goal is to shrink these over time and
|
||||
* eventually remove entries as logic moves into vscode-free modules.
|
||||
*
|
||||
* When you extract code out of one of these files, lower its maxLines to
|
||||
* the new line count rounded up to the nearest 50.
|
||||
*/
|
||||
const VSCODE_ALLOWED: Record<string, { maxLines: number; note: string }> = {
|
||||
// God class — decompose into WorktreeOrchestrator, DiffManager, ApplyManager, etc.
|
||||
"AgentManagerProvider.ts": {
|
||||
maxLines: 1900,
|
||||
note: "primary extraction target: break into vscode-free orchestrators",
|
||||
},
|
||||
// Thin adapter: wraps vscode.window terminal APIs behind TerminalHost interface
|
||||
"terminal-host.ts": {
|
||||
maxLines: 60,
|
||||
note: "vscode adapter for SessionTerminalManager",
|
||||
},
|
||||
// Thin adapter: wraps vscode.tasks API behind RunTask callback
|
||||
"task-runner.ts": {
|
||||
maxLines: 80,
|
||||
note: "vscode adapter for SetupScriptRunner",
|
||||
},
|
||||
}
|
||||
|
||||
function importsVscode(content: string): boolean {
|
||||
return /(?:from|require\()\s*["']vscode["']/.test(content)
|
||||
}
|
||||
|
||||
function agentManagerSourceFiles(): string[] {
|
||||
return fs
|
||||
.readdirSync(AGENT_MANAGER_DIR)
|
||||
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts"))
|
||||
}
|
||||
|
||||
describe("Agent Manager — VS Code import boundary", () => {
|
||||
it("only allowlisted files may import vscode", () => {
|
||||
const violations: string[] = []
|
||||
for (const file of agentManagerSourceFiles()) {
|
||||
if (file in VSCODE_ALLOWED) continue
|
||||
const content = fs.readFileSync(path.join(AGENT_MANAGER_DIR, file), "utf-8")
|
||||
if (importsVscode(content)) violations.push(file)
|
||||
}
|
||||
expect(
|
||||
violations,
|
||||
`These files import "vscode" but are not on the exception list.\n` +
|
||||
`Either extract the vscode dependency or add them to VSCODE_ALLOWED:\n` +
|
||||
violations.map((v) => ` - ${v}`).join("\n"),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it("allowlisted files stay within their maxLines cap", () => {
|
||||
const overweight: string[] = []
|
||||
for (const [file, { maxLines }] of Object.entries(VSCODE_ALLOWED)) {
|
||||
const filepath = path.join(AGENT_MANAGER_DIR, file)
|
||||
if (!fs.existsSync(filepath)) continue
|
||||
const lines = fs.readFileSync(filepath, "utf-8").split("\n").length
|
||||
if (lines > maxLines) overweight.push(`${file}: ${lines} lines (maxLines: ${maxLines})`)
|
||||
}
|
||||
expect(
|
||||
overweight,
|
||||
`These VS Code integration files exceed their maxLines cap.\n` +
|
||||
`Extract business logic into vscode-free modules and lower maxLines:\n` +
|
||||
overweight.map((o) => ` - ${o}`).join("\n"),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it("every allowlisted file actually exists", () => {
|
||||
const stale = Object.keys(VSCODE_ALLOWED).filter((f) => !fs.existsSync(path.join(AGENT_MANAGER_DIR, f)))
|
||||
expect(
|
||||
stale,
|
||||
`These files are in VSCODE_ALLOWED but no longer exist — remove them:\n` +
|
||||
stale.map((s) => ` - ${s}`).join("\n"),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it("every allowlisted file actually imports vscode", () => {
|
||||
const unnecessary: string[] = []
|
||||
for (const file of Object.keys(VSCODE_ALLOWED)) {
|
||||
const filepath = path.join(AGENT_MANAGER_DIR, file)
|
||||
if (!fs.existsSync(filepath)) continue
|
||||
if (!importsVscode(fs.readFileSync(filepath, "utf-8"))) unnecessary.push(file)
|
||||
}
|
||||
expect(
|
||||
unnecessary,
|
||||
`These files no longer import "vscode" — remove them from VSCODE_ALLOWED:\n` +
|
||||
unnecessary.map((u) => ` - ${u}`).join("\n"),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -252,90 +252,67 @@ describe("GitOps", () => {
|
||||
})
|
||||
|
||||
describe("aheadBehind", () => {
|
||||
it("counts commits ahead and behind upstream", async () => {
|
||||
it("counts commits ahead and behind using the provided ref", async () => {
|
||||
const git = ops(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "branch") return "feature"
|
||||
if (args[0] === "config") return "origin"
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "1\t3"
|
||||
return ""
|
||||
})
|
||||
expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 3, behind: 1 })
|
||||
expect(await git.aheadBehind("/repo", "origin/main")).toEqual({ ahead: 3, behind: 1 })
|
||||
})
|
||||
|
||||
it("uses resolved remote for non-origin setups", async () => {
|
||||
it("fetches the explicitly-provided remote before counting", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
commands.push(args)
|
||||
// no upstream configured
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}")
|
||||
throw new Error("no upstream")
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") throw new Error("no upstream")
|
||||
if (args[0] === "branch") return "feature"
|
||||
// branch.feature.remote = myfork
|
||||
if (args[0] === "config" && args[1] === "branch.feature.remote") return "myfork"
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
// myfork/feature exists
|
||||
if (
|
||||
args[0] === "rev-parse" &&
|
||||
args[1] === "--verify" &&
|
||||
args[2] === "--quiet" &&
|
||||
args[3] === "refs/remotes/myfork/feature"
|
||||
)
|
||||
return "abc"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t4"
|
||||
return ""
|
||||
})
|
||||
expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 4, behind: 0 })
|
||||
expect(await git.aheadBehind("/repo", "myfork/main", "myfork")).toEqual({ ahead: 4, behind: 0 })
|
||||
const fetches = commands.filter((c) => c[0] === "fetch")
|
||||
expect(fetches.length).toBe(1)
|
||||
expect(fetches[0]![3]).toBe("myfork")
|
||||
})
|
||||
|
||||
it("falls back to remote/parentBranch when no upstream and no remote branch", async () => {
|
||||
it("skips fetch when no remote is provided", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}")
|
||||
throw new Error("no upstream")
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") throw new Error("no upstream")
|
||||
if (args[0] === "branch") return "feature"
|
||||
if (args[0] === "config") return "origin"
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (
|
||||
args[0] === "rev-parse" &&
|
||||
args[1] === "--verify" &&
|
||||
args[2] === "--quiet" &&
|
||||
args[3] === "refs/remotes/origin/feature"
|
||||
)
|
||||
throw new Error("no ref")
|
||||
if (
|
||||
args[0] === "rev-parse" &&
|
||||
args[1] === "--verify" &&
|
||||
args[2] === "--quiet" &&
|
||||
args[3] === "refs/remotes/origin/main"
|
||||
)
|
||||
return "abc"
|
||||
if (args[0] === "fetch") return ""
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t2"
|
||||
return ""
|
||||
})
|
||||
expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 2, behind: 0 })
|
||||
const fetches = commands.filter((c) => c[0] === "fetch")
|
||||
expect(fetches.length).toBe(0)
|
||||
})
|
||||
|
||||
it("returns zeros when rev-list fails", async () => {
|
||||
const git = ops(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "branch") return "feature"
|
||||
if (args[0] === "config") return "origin"
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list") throw new Error("fatal")
|
||||
return ""
|
||||
})
|
||||
expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 0, behind: 0 })
|
||||
expect(await git.aheadBehind("/repo", "origin/main")).toEqual({ ahead: 0, behind: 0 })
|
||||
})
|
||||
|
||||
it("uses the ref directly without double-prefixing", async () => {
|
||||
const refs: string[] = []
|
||||
const git = ops(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") {
|
||||
refs.push(args[3]!)
|
||||
return "0\t1"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
const result = await git.aheadBehind("/repo", "origin/main")
|
||||
expect(result).toEqual({ ahead: 1, behind: 0 })
|
||||
expect(refs[0]).toBe("origin/main...HEAD")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ async function waitFor(check: () => boolean, timeout = 500): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function worktree(id: string): Worktree {
|
||||
function worktree(id: string, remote = "origin"): Worktree {
|
||||
return {
|
||||
id,
|
||||
branch: `branch-${id}`,
|
||||
path: `/tmp/${id}`,
|
||||
parentBranch: "main",
|
||||
remote,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
}
|
||||
}
|
||||
@@ -415,8 +416,9 @@ describe("GitStatsPoller", () => {
|
||||
worktree: { diff: async () => ({ data: diff(0, 0) }) },
|
||||
} as unknown as KiloClient
|
||||
|
||||
// Worktrees store remote="upstream" so aheadBehind receives "upstream/main"
|
||||
const poller = new GitStatsPoller({
|
||||
getWorktrees: () => [worktree("a"), worktree("b")],
|
||||
getWorktrees: () => [worktree("a", "upstream"), worktree("b", "upstream")],
|
||||
getWorkspaceRoot: () => undefined,
|
||||
getClient: () => client,
|
||||
onStats: (stats) => emitted.push(stats),
|
||||
@@ -426,9 +428,6 @@ describe("GitStatsPoller", () => {
|
||||
git: gitOps(async (args) => {
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "upstream/main"
|
||||
if (args[0] === "branch") return "feature"
|
||||
if (args[0] === "config") return "origin"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0"
|
||||
return ""
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
EventMessageUpdated,
|
||||
EventSessionStatus,
|
||||
EventPermissionAsked,
|
||||
EventPermissionReplied,
|
||||
EventTodoUpdated,
|
||||
EventQuestionAsked,
|
||||
EventQuestionReplied,
|
||||
@@ -302,6 +303,42 @@ describe("mapSSEEventToWebviewMessage", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("maps permission.replied to permissionResolved", () => {
|
||||
const event: EventPermissionReplied = {
|
||||
type: "permission.replied",
|
||||
properties: { sessionID: "sess-1", requestID: "perm-1", reply: "once" },
|
||||
}
|
||||
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
|
||||
expect(msg?.type).toBe("permissionResolved")
|
||||
if (msg?.type === "permissionResolved") {
|
||||
expect(msg.permissionID).toBe("perm-1")
|
||||
}
|
||||
})
|
||||
|
||||
it("maps permission.replied (always) to permissionResolved", () => {
|
||||
const event: EventPermissionReplied = {
|
||||
type: "permission.replied",
|
||||
properties: { sessionID: "sess-1", requestID: "perm-2", reply: "always" },
|
||||
}
|
||||
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
|
||||
expect(msg?.type).toBe("permissionResolved")
|
||||
if (msg?.type === "permissionResolved") {
|
||||
expect(msg.permissionID).toBe("perm-2")
|
||||
}
|
||||
})
|
||||
|
||||
it("maps permission.replied (reject) to permissionResolved", () => {
|
||||
const event: EventPermissionReplied = {
|
||||
type: "permission.replied",
|
||||
properties: { sessionID: "sess-1", requestID: "perm-3", reply: "reject" },
|
||||
}
|
||||
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
|
||||
expect(msg?.type).toBe("permissionResolved")
|
||||
if (msg?.type === "permissionResolved") {
|
||||
expect(msg.permissionID).toBe("perm-3")
|
||||
}
|
||||
})
|
||||
|
||||
it("maps todo.updated to todoUpdated", () => {
|
||||
const event: EventTodoUpdated = {
|
||||
type: "todo.updated",
|
||||
|
||||
@@ -34,8 +34,8 @@ describe("SessionTerminalManager structure", () => {
|
||||
expect(ctor).toBeTruthy()
|
||||
const text = ctor!.getText()
|
||||
// Both listeners are required: close (cleanup) and active-change (context key)
|
||||
expect(text).toContain("onDidCloseTerminal")
|
||||
expect(text).toContain("onDidChangeActiveTerminal")
|
||||
expect(text).toContain("onTerminalClosed")
|
||||
expect(text).toContain("onActiveTerminalChanged")
|
||||
})
|
||||
|
||||
it("dispose clears the context key, disposes terminals, and clears the map", () => {
|
||||
|
||||
@@ -37,6 +37,37 @@ function createManager(root: string): WorktreeManager {
|
||||
return new WorktreeManager(root, (msg) => logs.push(msg))
|
||||
}
|
||||
|
||||
/** Create a temp repo with a bare origin remote so origin/<branch> refs exist. */
|
||||
async function createTempRepoWithOrigin(): Promise<{ bare: string; clone: string }> {
|
||||
// Use a non-bare seed repo to control the initial branch name, then clone bare
|
||||
const seed = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-seed-"))
|
||||
tempDirs.push(seed)
|
||||
const seedGit = simpleGit(seed)
|
||||
await seedGit.init()
|
||||
await seedGit.addConfig("user.email", "test@test.com")
|
||||
await seedGit.addConfig("user.name", "Test")
|
||||
await fs.writeFile(path.join(seed, "README.md"), "init")
|
||||
await seedGit.add(".")
|
||||
await seedGit.commit("initial commit")
|
||||
// Ensure branch is named "main" regardless of system default
|
||||
const seedBranch = (await seedGit.revparse(["--abbrev-ref", "HEAD"])).trim()
|
||||
if (seedBranch !== "main") await seedGit.raw(["branch", "-m", seedBranch, "main"])
|
||||
|
||||
// Clone to bare, then clone again as working copy
|
||||
const bare = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-bare-"))
|
||||
tempDirs.push(bare)
|
||||
await simpleGit().clone(seed, bare, ["--bare"])
|
||||
|
||||
const clone = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-wt-clone-"))
|
||||
tempDirs.push(clone)
|
||||
await simpleGit().clone(bare, clone)
|
||||
const cloneGit = simpleGit(clone)
|
||||
await cloneGit.addConfig("user.email", "test@test.com")
|
||||
await cloneGit.addConfig("user.name", "Test")
|
||||
|
||||
return { bare, clone }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// generateBranchName
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -728,6 +759,44 @@ describe("WorktreeManager.resolveStartPoint", () => {
|
||||
expect(res.ref).toBe(head)
|
||||
})
|
||||
|
||||
it("returns bare branch + remote when remote exists", async () => {
|
||||
const { clone } = await createTempRepoWithOrigin()
|
||||
const mgr = createManager(clone)
|
||||
|
||||
const res = await mgr.resolveStartPoint("main")
|
||||
expect(res.source).toBe("remote")
|
||||
expect(res.ref).toBe("origin/main")
|
||||
expect(res.branch).toBe("main")
|
||||
expect(res.remote).toBe("origin")
|
||||
})
|
||||
|
||||
it("returns bare branch + remote for stale tracking ref", async () => {
|
||||
const { clone } = await createTempRepoWithOrigin()
|
||||
const git = simpleGit(clone)
|
||||
// Remove origin so fetch fails, but the local tracking ref remains
|
||||
await git.removeRemote("origin")
|
||||
const mgr = createManager(clone)
|
||||
|
||||
const res = await mgr.resolveStartPoint("main")
|
||||
// After removing the remote, resolveRemote() returns undefined,
|
||||
// so "origin/main" won't be tried as ${remote}/${branch}. Falls back to local.
|
||||
expect(res.source).toBe("local-branch")
|
||||
expect(res.branch).toBe("main")
|
||||
expect(res.remote).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns bare branch name for local-only source", async () => {
|
||||
const root = await createTempRepo()
|
||||
const git = simpleGit(root)
|
||||
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
|
||||
const mgr = createManager(root)
|
||||
|
||||
const res = await mgr.resolveStartPoint(head)
|
||||
expect(res.source).toBe("local-branch")
|
||||
expect(res.branch).toBe(head)
|
||||
expect(res.remote).toBeUndefined()
|
||||
})
|
||||
|
||||
it("falls back to default branch when requested does not exist", async () => {
|
||||
const root = await createTempRepo()
|
||||
const git = simpleGit(root)
|
||||
@@ -750,6 +819,45 @@ describe("WorktreeManager.resolveStartPoint", () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WorktreeManager -- resolveBaseBranch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("WorktreeManager.resolveBaseBranch", () => {
|
||||
it("returns bare branch + remote when origin remote and tracking ref exist", async () => {
|
||||
const { clone } = await createTempRepoWithOrigin()
|
||||
const mgr = createManager(clone)
|
||||
|
||||
const result = await mgr.resolveBaseBranch()
|
||||
expect(result).toEqual({ branch: "main", remote: "origin" })
|
||||
})
|
||||
|
||||
it("returns bare branch without remote when no origin remote exists", async () => {
|
||||
const root = await createTempRepo()
|
||||
const mgr = createManager(root)
|
||||
|
||||
const result = await mgr.resolveBaseBranch()
|
||||
const git = simpleGit(root)
|
||||
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
|
||||
expect(result).toEqual({ branch: head })
|
||||
expect(result.remote).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns bare branch without remote when origin exists but tracking ref does not", async () => {
|
||||
const root = await createTempRepo()
|
||||
const git = simpleGit(root)
|
||||
// Add a remote that points nowhere — origin exists but origin/main ref doesn't
|
||||
await git.addRemote("origin", "https://example.com/repo.git")
|
||||
const mgr = createManager(root)
|
||||
|
||||
const result = await mgr.resolveBaseBranch()
|
||||
const git2 = simpleGit(root)
|
||||
const head = (await git2.revparse(["--abbrev-ref", "HEAD"])).trim()
|
||||
expect(result).toEqual({ branch: head })
|
||||
expect(result.remote).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("WorktreeManager.createWorktree advanced", () => {
|
||||
it("returns startPointSource in result", async () => {
|
||||
const root = await createTempRepo()
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3774fc422d473693e38fafba61c00632fd49d2a0c61d2332b9c31482af78a2b
|
||||
size 15268
|
||||
oid sha256:63915e70810da741acb2ab50cc3d557d58ec359f8b7b5e90b85d1c8e8f54d476
|
||||
size 17823
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d69f44620c8e3a2f8d54ec97903c95cde04bef4655fc3b280a318e6da3cb0b5
|
||||
size 9082
|
||||
oid sha256:4101f60556f1c55798df5aca5276449c42d12360419bb5596b7c3bd2548afa34
|
||||
size 9461
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc5f6d5b31970c33aaf062756bbdd73637db17f1bc74c1558e9a81c33c5d602d
|
||||
size 4582
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user