mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into mark/reimplement-mcp-removal
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
run-name: "${{ format('{0} {1}', inputs.pre_release && 'pre-release' || 'release', inputs.bump) }}"
|
||||
|
||||
on:
|
||||
# push:
|
||||
@@ -22,6 +22,11 @@ on:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
pre_release:
|
||||
description: "Publish as pre-release (VS Code marketplace)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
@@ -117,6 +122,7 @@ jobs:
|
||||
env:
|
||||
CLI_DIST_DIR: ../../packages/opencode/dist
|
||||
KILO_VERSION: ${{ needs.build-cli.outputs.version }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -394,6 +400,7 @@ jobs:
|
||||
env:
|
||||
KILO_VERSION: ${{ needs.version.outputs.version }}
|
||||
KILO_RELEASE: ${{ needs.version.outputs.release }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
+4
-1
@@ -4,7 +4,10 @@ node_modules
|
||||
.worktrees
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/gradle.xml
|
||||
!.idea/.run
|
||||
.idea/*.iml
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/backend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/frontend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/shared" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run JetBrains Plugin" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runIde" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -150,28 +150,54 @@
|
||||
'';
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
gh
|
||||
playwright-driver.browsers
|
||||
vsce
|
||||
unzip
|
||||
gnutar
|
||||
gzip
|
||||
patchelf
|
||||
ripgrep
|
||||
kilo-dev
|
||||
kilo-install-bin
|
||||
kilo-bin
|
||||
];
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
gh
|
||||
playwright-driver.browsers
|
||||
vsce
|
||||
unzip
|
||||
gnutar
|
||||
gzip
|
||||
patchelf
|
||||
ripgrep
|
||||
jetbrains.jdk
|
||||
jdk21
|
||||
kilo-dev
|
||||
kilo-install-bin
|
||||
kilo-bin
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
libX11
|
||||
libXext
|
||||
libXrender
|
||||
libXtst
|
||||
libXi
|
||||
fontconfig
|
||||
freetype
|
||||
];
|
||||
shellHook = ''
|
||||
export KILO_ROOT="$PWD"
|
||||
export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}"
|
||||
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
|
||||
''
|
||||
+ pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
||||
export LD_LIBRARY_PATH="${
|
||||
pkgs.lib.makeLibraryPath [
|
||||
pkgs.libX11
|
||||
pkgs.libXext
|
||||
pkgs.libXrender
|
||||
pkgs.libXtst
|
||||
pkgs.libXi
|
||||
pkgs.fontconfig
|
||||
pkgs.freetype
|
||||
]
|
||||
}:$LD_LIBRARY_PATH"
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-4yd8H4UN7YIhdIQ607UoxSqwsQEKqUWkOifjQ2PEvn0=",
|
||||
"aarch64-linux": "sha256-EuzzDc/j63eRV8QEFgDmQRIsiSq4ZPr9RJSdB9dH+kI=",
|
||||
"aarch64-darwin": "sha256-BLCZubMC/n06HsUeO3FsvREwfsmjtiTBseSMDPPZNYM=",
|
||||
"x86_64-darwin": "sha256-gU1uhExpYJhhVHbspCwCry/DwUiuddRvYl72RZqWzNo="
|
||||
"x86_64-linux": "sha256-PvqhO2tOClenS37A80VsNuOglBs5fk8aaAtsktwG74s=",
|
||||
"aarch64-linux": "sha256-+UN4dTY2s7GWjYumPoKTSv2pX7zhJB51hxJzCwqejaE=",
|
||||
"aarch64-darwin": "sha256-N5JmmR6XrzGT86AL6TxpcQoZl7PTmgfd6ll5CVamQT0=",
|
||||
"x86_64-darwin": "sha256-eyLUps3Kzg+6/VPt/XW78+vaL+9RY1aTtnnPxUPSkWQ="
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -38,12 +38,12 @@
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"diff": "8.0.4",
|
||||
"dompurify": "3.3.3",
|
||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||
"ai": "5.0.124",
|
||||
"hono": "4.10.7",
|
||||
"hono": "4.12.12",
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
@@ -58,11 +58,11 @@
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"virtua": "0.42.3",
|
||||
"vite": "7.1.4",
|
||||
"vite": "7.3.2",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-js": "1.9.12",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
}
|
||||
},
|
||||
@@ -79,7 +79,7 @@
|
||||
"turbo": "2.8.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@aws-sdk/client-s3": "3.1025.0",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -105,13 +105,23 @@
|
||||
],
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
"@types/node": "catalog:",
|
||||
"path-to-regexp": ">=8.4.0",
|
||||
"picomatch": ">=2.3.2",
|
||||
"defu": "6.1.6",
|
||||
"lodash": "4.18.1",
|
||||
"@xmldom/xmldom": ">=0.8.12",
|
||||
"smol-toml": ">=1.6.1",
|
||||
"fastify": ">=5.8.3",
|
||||
"diff": "8.0.4",
|
||||
"dompurify": "3.3.3",
|
||||
"happy-dom": ">=20.8.9"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@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.1.19",
|
||||
"version": "7.1.22",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"electron": "40.4.1",
|
||||
"electron": "40.8.5",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.1.19"
|
||||
version = "7.1.22"
|
||||
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.1.19/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.22/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.1.19/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.22/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.19/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.22/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.1.19/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.22/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.1.19/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.22/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from "react"
|
||||
|
||||
interface VideoEmbedProps {
|
||||
url: string
|
||||
title?: string
|
||||
caption?: string
|
||||
}
|
||||
|
||||
function extractVideoId(url: string): string | null {
|
||||
const patterns = [
|
||||
/(?:youtube\.com\/watch\?v=)([^&\s]+)/,
|
||||
/(?:youtube\.com\/embed\/)([^?\s]+)/,
|
||||
/(?:youtu\.be\/)([^?\s]+)/,
|
||||
/(?:youtube\.com\/v\/)([^?\s]+)/,
|
||||
]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = url.match(pattern)
|
||||
if (match) {
|
||||
return match[1] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function VideoEmbed({ url, title = "Video", caption }: VideoEmbedProps) {
|
||||
const videoId = extractVideoId(url)
|
||||
|
||||
if (!videoId) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
backgroundColor: "var(--red-100, #fee2e2)",
|
||||
color: "var(--red-700, #b91c1c)",
|
||||
borderRadius: "0.5rem",
|
||||
margin: "1.5rem 0",
|
||||
}}
|
||||
>
|
||||
Invalid YouTube URL: {url}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "640px",
|
||||
margin: "1.5rem 0",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
paddingBottom: "56.25%",
|
||||
height: 0,
|
||||
overflow: "hidden",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
title={title}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
{caption && (
|
||||
<figcaption
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginTop: "0.5rem",
|
||||
color: "var(--gray-600, #6b7280)",
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</figcaption>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,4 +14,5 @@ export * from "./TableOfContents"
|
||||
export * from "./Tabs"
|
||||
export * from "./ThemeToggle"
|
||||
export * from "./TopNav"
|
||||
export * from "./VideoEmbed"
|
||||
export * from "./YouTube"
|
||||
|
||||
@@ -43,6 +43,7 @@ export const KiloClawNav: NavSection[] = [
|
||||
subLinks: [
|
||||
{ href: "/kiloclaw/triggers", children: "Overview" },
|
||||
{ href: "/kiloclaw/triggers/webhooks", children: "Webhooks" },
|
||||
{ href: "/kiloclaw/triggers/scheduled", children: "Scheduled" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,4 +36,7 @@ exclude = [
|
||||
'^https?://api\.apertis\.ai/v1/?$',
|
||||
# Redirects to authenticated Google Cloud console
|
||||
'^https?://console\.cloud\.google\.com',
|
||||
# Consistently times out in CI
|
||||
'^https?://opncd\.ai',
|
||||
'^https?://zod\.dev/v4/changelog',
|
||||
]
|
||||
|
||||
@@ -5,4 +5,6 @@ export * from "./tabs.markdoc"
|
||||
export * from "./icon.markdoc"
|
||||
export * from "./image.markdoc"
|
||||
export * from "./kilo-code-icon.markdoc"
|
||||
export * from "./video.markdoc"
|
||||
export * from "./youtube.markdoc"
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { VideoEmbed } from "../../components"
|
||||
|
||||
export const video = {
|
||||
render: VideoEmbed,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "The YouTube video URL",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
description: "Accessible title for the video iframe",
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
description: "Optional caption to display below the video",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -56,19 +56,32 @@ Once configured, the Review Agent runs automatically on PR/MR events. For platfo
|
||||
|
||||
Code Reviewer is also available locally. This is valuable for developers who want to review their code before pushing a pull request to their team publicly, or for developers who want reviews and don't need to ship a pull request to GitHub.
|
||||
|
||||
### VS Code
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
Select 'Review' from the mode dropdown after making local changes, and click 'Send' for AI-powered feedback and suggestions.
|
||||
|
||||

|
||||
|
||||
### CLI
|
||||
|
||||
The CLI provides two commands for local code reviews:
|
||||
Two slash commands are available for local code reviews:
|
||||
|
||||
- **`/local-review`** — Review all changes on your current branch vs the base branch
|
||||
- **`/local-review-uncommitted`** — Review uncommitted changes (staged + unstaged)
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Two slash commands are available for local code reviews:
|
||||
|
||||
- **`/local-review`** — Review all changes on your current branch vs the base branch
|
||||
- **`/local-review-uncommitted`** — Review uncommitted changes (staged + unstaged)
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
|
||||
Select 'Review' from the mode dropdown after making local changes, and click 'Send' for AI-powered feedback and suggestions.
|
||||
|
||||
{% image src="/docs/img/code-reviewer/review-mode.png" alt="VS Code interface showing Review option in mode dropdown" width="800" caption="Review Mode" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## How Code Reviews Work
|
||||
|
||||
When a pull request or merge request is opened or updated:
|
||||
|
||||
@@ -18,27 +18,6 @@ Kilo Code ships with a curated list of models for each provider, but you can use
|
||||
Add custom models under the `provider.<provider_id>.models` key in your config file. The model key becomes the model ID you reference elsewhere.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
**Config file** (`~/.config/kilo/kilo.jsonc` or `./kilo.jsonc`):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://app.kilo.ai/config.json",
|
||||
"model": "lmstudio/my-custom-model",
|
||||
"provider": {
|
||||
"lmstudio": {
|
||||
"models": {
|
||||
"my-custom-model": {
|
||||
"name": "My Custom Model",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
1. Open **Settings** (gear icon) and go to the **Providers** tab.
|
||||
@@ -64,6 +43,27 @@ To edit an existing custom provider, click the **Edit provider** button next to
|
||||
|
||||
For additional model configuration (token limits, tool calling, reasoning, variants), edit the `kilo.jsonc` config file directly — see the **CLI** tab for the format.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
**Config file** (`~/.config/kilo/kilo.jsonc` or `./kilo.jsonc`):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://app.kilo.ai/config.json",
|
||||
"model": "lmstudio/my-custom-model",
|
||||
"provider": {
|
||||
"lmstudio": {
|
||||
"models": {
|
||||
"my-custom-model": {
|
||||
"name": "My Custom Model",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
@@ -91,6 +91,43 @@ All fields are optional. When a model ID matches one already in the built-in cat
|
||||
| `provider` | `object` | Override `{ npm?, api? }` — the AI SDK package or base API URL for this model |
|
||||
| `variants` | `object` | Named variant configurations (e.g., different reasoning efforts) |
|
||||
|
||||
### Token Limits (limit)
|
||||
|
||||
The `limit` object controls how Kilo manages the model's context window and output length. These values are specified in **tokens**.
|
||||
|
||||
| Sub-field | Type | Required | Description |
|
||||
| --------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `context` | `number` | No | The model's total context window size (e.g., `131072` for a 128K model). Used to determine when conversation history should be compacted to stay within the window. |
|
||||
| `output` | `number` | No | The maximum number of tokens the model can generate in a single response. Sent to the provider as `max_tokens` or equivalent. Capped at 32,000 by default. |
|
||||
| `input` | `number` | No | An optional stricter input limit. Some providers enforce an input token ceiling that is lower than the full context window. When set, compaction triggers against this value instead of `context`. |
|
||||
|
||||
```jsonc
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
```
|
||||
|
||||
#### How limits are resolved
|
||||
|
||||
Kilo resolves token limits in this order:
|
||||
|
||||
1. **Your config** — values you set under `provider.<id>.models.<model>.limit`
|
||||
2. **Built-in catalog** — Kilo ships a snapshot of [models.dev](https://models.dev) and refreshes it hourly. If your model ID matches a known model, catalog values are used as defaults.
|
||||
3. **Fallback** — if neither source provides a value, `context` and `output` default to `0`.
|
||||
|
||||
#### What happens when limits are `0`
|
||||
|
||||
If you use a custom or local model and don't specify limits — and the model isn't in the built-in catalog — both `context` and `output` resolve to `0`. This has meaningful side effects:
|
||||
|
||||
- **Compaction is disabled.** Kilo uses `context` to detect when the conversation exceeds the model's window and needs to be summarized. With `context: 0`, overflow detection is skipped and conversations will grow unbounded until the provider rejects the request.
|
||||
- **Output falls back to 32,000 tokens.** When `output` is `0`, Kilo uses its internal default of 32,000 tokens (configurable via the `KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX` environment variable).
|
||||
- **No context usage tracking.** Usage metrics that depend on knowing the context size are skipped.
|
||||
|
||||
{% callout type="warning" %}
|
||||
For custom and local models, always set `limit.context` and `limit.output` to match the model's actual capabilities. Without these values, automatic context management is disabled.
|
||||
{% /callout %}
|
||||
|
||||
## Examples
|
||||
|
||||
### Local model with LM Studio
|
||||
@@ -226,7 +263,7 @@ Override options or define reasoning variants for a built-in model:
|
||||
}
|
||||
```
|
||||
|
||||
### Using the `id` field to map model names
|
||||
### Using the id field to map model names
|
||||
|
||||
If the model key in your config differs from what the provider expects, use the `id` field:
|
||||
|
||||
@@ -313,5 +350,6 @@ Control which models appear in the model picker for a provider using allowlists
|
||||
**Model errors or unexpected behavior:**
|
||||
|
||||
- Set `tool_call: true` if you need the model to use tools (file editing, terminal, etc.)
|
||||
- Set `limit.context` and `limit.output` to match the model's actual capabilities
|
||||
- Set `limit.context` and `limit.output` to match the model's actual capabilities — see [Token Limits](#token-limits-limit) above for details and defaults
|
||||
- If conversations seem to grow without being compacted, your `limit.context` is likely `0` (unset)
|
||||
- For local models, ensure your inference server is running and accessible at the configured URL
|
||||
|
||||
@@ -2,6 +2,80 @@
|
||||
|
||||
This guide walks you through setting up Mistral's Codestral model for free autocomplete in Kilo Code. Mistral offers a free tier that's perfect for getting started with AI-powered code completions.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VS Code" %}
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A [Kilo Code account](https://app.kilo.ai) (free to create)
|
||||
- A Mistral AI account with a Codestral API key
|
||||
|
||||
## Step 1: Navigate to Codestral in Mistral AI Studio
|
||||
|
||||
Go to the [Mistral AI console](https://console.mistral.ai/) and sign up or sign in to your account. In the sidebar, click **Codestral** under the Code section.
|
||||
|
||||

|
||||
|
||||
## Step 2: Generate API Key
|
||||
|
||||
Click the **Generate API Key** button to create your new Codestral API key.
|
||||
|
||||

|
||||
|
||||
## Step 3: Copy Your API Key
|
||||
|
||||
Once generated, click the **copy** button next to your API key to copy it to your clipboard.
|
||||
|
||||

|
||||
|
||||
{% callout type="note" %}
|
||||
The Codestral API key is separate from the standard Mistral La Plateforme API key. Make sure you generate a key specifically from the **Codestral** section of the Mistral console.
|
||||
{% /callout %}
|
||||
|
||||
## Step 4: Add Your Key via BYOK in Kilo
|
||||
|
||||
1. Log into the [Kilo platform](https://app.kilo.ai).
|
||||
2. Navigate to the [Bring Your Own Key (BYOK) page](https://app.kilo.ai/byok), available in the sidebar under **Account**.
|
||||
3. Click **Add Your First Key** (or **Add Key** if you already have keys configured).
|
||||
4. Select **Codestral** as the provider.
|
||||
5. Paste your Codestral API key.
|
||||
6. Click **Save**.
|
||||
|
||||
{% callout type="tip" %}
|
||||
For more details on BYOK, see the [Bring Your Own Key documentation](/docs/getting-started/byok).
|
||||
{% /callout %}
|
||||
|
||||
## Step 5: Verify Autocomplete is Working
|
||||
|
||||
Once your BYOK key is saved, Kilo Code's autocomplete will automatically use your Codestral key through the Kilo Gateway. No additional configuration is needed in the extension.
|
||||
|
||||
1. Open VS Code with the Kilo Code extension installed.
|
||||
2. Start typing in any code file — you should see inline ghost-text suggestions powered by Codestral.
|
||||
3. Press `Tab` to accept a suggestion.
|
||||
|
||||
The autocomplete status bar in VS Code shows the current provider ("Kilo Gateway") and tracks cumulative cost. With BYOK, requests are billed directly by Mistral at their rates (Codestral has a free tier) and show as $0.00 on your Kilo balance.
|
||||
|
||||
## How It Works
|
||||
|
||||
When you add a Codestral BYOK key, the request flow is:
|
||||
|
||||
```
|
||||
Your Editor → Kilo Gateway (with your key) → Mistral
|
||||
```
|
||||
|
||||
- The Kilo Gateway detects your BYOK key and routes autocomplete requests using it.
|
||||
- You are billed directly by Mistral — Kilo does not add any markup.
|
||||
- If your BYOK key is invalid, the request will fail (it does not fall back to Kilo's keys).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Autocomplete not appearing?** Check that autocomplete is enabled in Kilo Code settings (it is on by default). Also verify you are signed into Kilo Code in the extension.
|
||||
- **Key not working?** Ensure you copied the **Codestral** API key (not the standard La Plateforme key). You can verify your key at [console.mistral.ai/codestral](https://console.mistral.ai/codestral).
|
||||
- **Seeing charges on your Kilo balance?** If you haven't configured BYOK, autocomplete defaults to using your Kilo credits. Add your Codestral key via BYOK to route requests through your own Mistral account.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VS Code Legacy" %}
|
||||
|
||||
## Video Walkthrough
|
||||
|
||||
{% youtube url="https://www.youtube.com/embed/0aqBbB8fPho" caption="Setting up Mistral for free autocomplete in Kilo Code" /%}
|
||||
@@ -74,6 +148,9 @@ Click **Save** to apply your Mistral configuration. You're now ready to use free
|
||||
|
||||

|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn more about [Autocomplete features](/docs/code-with-ai/features/autocomplete)
|
||||
|
||||
@@ -112,30 +112,43 @@ Cloud Agents are great for:
|
||||
- **Automated refactors or tech debt cleanup** driven by Kilo Code
|
||||
- **Offloading CI-like tasks**, experiments, or batch updates
|
||||
|
||||
## Webhook Triggers
|
||||
## Triggers
|
||||
|
||||
Webhook triggers allow you to initiate cloud agent sessions via HTTP requests. This enables integration with external services and automation workflows.
|
||||
Triggers allow you to initiate cloud agent sessions automatically, either via HTTP requests (webhooks) or on a recurring schedule. This enables integration with external services and time-based automation workflows.
|
||||
|
||||
{% callout type="note" %}
|
||||
Webhook triggers are currently in beta and subject to change.
|
||||
Triggers are currently in beta and subject to change.
|
||||
{% /callout %}
|
||||
|
||||
### Accessing Webhooks
|
||||
### Accessing Triggers
|
||||
|
||||
Webhook triggers are accessible from the main sidebar with an entry named **Webhook** and link to [https://app.kilo.ai/cloud/webhooks](https://app.kilo.ai/cloud/webhooks) for personal accounts. Organization-level webhook configurations are available through your organization's sidebar.
|
||||
Triggers are accessible from the main sidebar under **Webhooks / Triggers** and link to [https://app.kilo.ai/cloud/triggers](https://app.kilo.ai/cloud/triggers) for personal accounts. Organization-level trigger configurations are available through your organization's sidebar.
|
||||
|
||||
### Activation Modes
|
||||
|
||||
When creating a trigger, you choose an **activation mode** that cannot be changed after creation:
|
||||
|
||||
- **Webhook**: Fires when an external service sends an HTTP request to the trigger's URL
|
||||
- **Scheduled**: Fires on a recurring schedule defined by a cron expression
|
||||
|
||||
### Configuration
|
||||
|
||||
Webhook triggers utilize [agent environment profiles](#agent-environment-profiles) to configure the execution environment for triggered sessions. The agent resolves the profile at runtime, so profile updates apply automatically to future executions. Profiles referenced by triggers cannot be deleted until those triggers are updated or removed.
|
||||
Triggers utilize [agent environment profiles](#agent-environment-profiles) to configure the execution environment for triggered sessions. The agent resolves the profile at runtime, so profile updates apply automatically to future executions. Profiles referenced by triggers cannot be deleted until those triggers are updated or removed.
|
||||
|
||||
Webhook triggers do not support manual env var or setup command overrides at this time.
|
||||
Triggers do not support manual env var or setup command overrides at this time.
|
||||
|
||||
### Scheduled Triggers
|
||||
|
||||
Scheduled triggers fire on a recurring schedule using cron expressions. You can configure them with a simple frequency picker (every 10 minutes, hourly, daily, weekly) or enter a raw cron expression for full control. Each trigger has a configurable timezone (default: UTC) and handles daylight saving time transitions automatically.
|
||||
|
||||
The minimum schedule interval is 10 minutes. Scheduled triggers use `{{scheduledTime}}` and `{{timestamp}}` as prompt template variables (webhook-specific variables like `{{body}}` are not available since there is no inbound HTTP request).
|
||||
|
||||
### Trigger Limits and Guidance
|
||||
|
||||
Webhook triggers are designed for low-volume invocations from trusted sources and are best suited for short-lived tasks.
|
||||
Triggers are designed for low-volume invocations from trusted sources and are best suited for short-lived tasks.
|
||||
|
||||
- **Personal webhooks**: Execute in the same sandbox container as a user's Cloud Agent sessions. You can view/join invocations live.
|
||||
- **Organization webhooks**: Execute in dedicated compute resources as a bot user, similar to Code Review sessions. You can share/fork the sessions when they're complete.
|
||||
- **Personal triggers**: Execute in the same sandbox container as a user's Cloud Agent sessions. You can view/join invocations live.
|
||||
- **Organization triggers**: Execute in dedicated compute resources as a bot user, similar to Code Review sessions. You can share/fork the sessions when they're complete.
|
||||
|
||||
Additional limits:
|
||||
|
||||
@@ -144,11 +157,13 @@ Additional limits:
|
||||
- **Retention**: only the **most recent 100 requests per trigger** are retained
|
||||
- **In-flight cap**: at most **20 requests per trigger** can be in `captured` or `inprogress` at once (returns `429`)
|
||||
|
||||
The webhook endpoint will return rate limit responses when the number of queued or processing requests exceeds system capacity.
|
||||
The trigger endpoint will return rate limit responses when the number of queued or processing requests exceeds system capacity.
|
||||
|
||||
### Webhook Prompt Template Variables
|
||||
### Prompt Template Variables
|
||||
|
||||
You can reference request data in a trigger’s prompt template using these placeholders:
|
||||
You can reference data in a trigger’s prompt template using these placeholders.
|
||||
|
||||
**Webhook triggers:**
|
||||
|
||||
- `{{body}}` - raw request body (string)
|
||||
- `{{bodyJson}}` - pretty-printed JSON if parseable, otherwise raw body
|
||||
@@ -159,6 +174,11 @@ You can reference request data in a trigger’s prompt template using these plac
|
||||
- `{{sourceIp}}` - client IP if provided (falls back to `unknown`)
|
||||
- `{{timestamp}}` - capture timestamp (ISO string)
|
||||
|
||||
**Scheduled triggers:**
|
||||
|
||||
- `{{scheduledTime}}` - the time the schedule fired (ISO string)
|
||||
- `{{timestamp}}` - capture timestamp (ISO string)
|
||||
|
||||
{% callout type="warning" title="Security Considerations" %}
|
||||
Care should be taken when deciding to use webhooks as they are susceptible to prompt injection attacks. Especially in scenarios where webhook payloads may contain untrusted input. At this time we recommend using webhooks only for trusted sources.
|
||||
{% /callout %}
|
||||
|
||||
@@ -37,8 +37,20 @@ See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actio
|
||||
|
||||
### Is the context progress graph still available?
|
||||
|
||||
The context progress graph is being evaluated for the new extension.
|
||||
This feature may be reintroduced in a future update.
|
||||
The context progress graph will be [added soon](https://github.com/Kilo-Org/kilocode/issues/8210) for users who like to see it.
|
||||
|
||||
### I like to closely monitor and approve the behavior of the agent. How can I do that better in the new version?
|
||||
|
||||
We are working to improve the experience in closely managing an agent. Identified improvements and progress are being tracked in a [GitHub issue](https://github.com/Kilo-Org/kilocode/issues/8415).
|
||||
|
||||
In the meantime we suggest exploring:
|
||||
|
||||
- [Auto-approval](https://kilo.ai/docs/getting-started/settings/auto-approving-actions) of actions: to control what the agent is allowed to do, and require approval when desired
|
||||
- [Agents](https://kilo.ai/docs/code-with-ai/agents/using-agents) (previously known as Modes): Managing the agent types in the extension, adding new ones, and setting the default models for each.
|
||||
|
||||
### How can I control which models each agent/mode uses?
|
||||
|
||||
Modes have been renamed to Agents in the new extension. You can set the default model for each agent in `Settings -> Models -> Model per Mode`. For more information please check the [agents documentation](https://kilo.ai/docs/code-with-ai/agents/using-agents).
|
||||
|
||||
### Where is the diff view for file changes?
|
||||
|
||||
@@ -50,6 +62,18 @@ The Agent Manager also includes a built-in diff reviewer that shows every change
|
||||
You can now trigger local AI-powered code reviews directly by using two commands: **`/local-review`** to review all changes on your current branch vs the base branch, and **`/local-review-uncommitted`** to review staged and unstaged changes.
|
||||
See the [Code Reviews](/docs/automate/code-reviews/overview) documentation for the full setup and options.
|
||||
|
||||
### How can I see the cost of each model?
|
||||
|
||||
In the model picker dropdown, click the expand button in the upper-right corner to switch to the full model picker view. From there, click on any model to see its details — including input and output pricing per million tokens, the context window size, and which capabilities the model supports (reasoning, text, images, etc.). This makes it easy to compare costs before selecting a model.
|
||||
|
||||
### How do I set context limits or other parameters for custom models?
|
||||
|
||||
If you're using a custom model (e.g. via your own API key or a self-hosted provider), you can configure the context window size, max output tokens, and other parameters in your model settings. See the [Custom Models](/docs/code-with-ai/agents/custom-models) documentation for the full guide on adding and configuring custom models.
|
||||
|
||||
### Where did my custom profiles go?
|
||||
|
||||
In the new extension we simplified the model selection by removing the profile layer. To keep models easily reachable you don't need a profile — you can just star them in the model selector to mark them as favorites.
|
||||
|
||||
### Where did orchestrator mode go?
|
||||
|
||||
Orchestrator mode is deprecated.
|
||||
|
||||
@@ -20,9 +20,9 @@ Because Autocomplete needs to be ready the moment you start typing, the model st
|
||||
|
||||
#### How much does it cost?
|
||||
|
||||
You can use Codestral 2508 for Autocomplete completely for free by configuring it through our Mistral integration.
|
||||
You can use Codestral for Autocomplete without consuming Kilo credits by adding your own Mistral Codestral API key via BYOK (Bring Your Own Key). Mistral offers a free tier for Codestral.
|
||||
|
||||
**Setup Guide:** [Setting up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup#setting-up-mistral-for-free-autocomplete)
|
||||
**Setup Guide:** [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup)
|
||||
|
||||
#### How to Disable These Requests
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ If you run out of credits and haven't configured a free alternative, autocomplet
|
||||
|
||||
### How to Get It Free
|
||||
|
||||
Configure Mistral directly as your autocomplete provider. Mistral offers free access to their Codestral model, which is optimized for code completions. When you configure Mistral directly, it takes precedence over the default Kilo Code routing.
|
||||
Add your own Mistral Codestral API key via **BYOK (Bring Your Own Key)** on the Kilo Gateway. Mistral offers a free tier for Codestral, and when you configure a BYOK key, autocomplete requests are routed using your key — billed directly by Mistral at $0 on your Kilo balance.
|
||||
|
||||
For step-by-step instructions with screenshots, see our [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
For step-by-step instructions, see our [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
|
||||
## Free CLI Background Tasks
|
||||
|
||||
@@ -99,6 +99,6 @@ Replace `your-preferred-free-model` with any free model available in the model p
|
||||
## Related Resources
|
||||
|
||||
- [Free and Budget Models](/docs/code-with-ai/agents/free-and-budget-models) - Complete guide to free and budget-friendly model options
|
||||
- [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup) - Step-by-step free autocomplete setup
|
||||
- [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup) - Step-by-step autocomplete setup via BYOK
|
||||
- [Autocomplete](/docs/code-with-ai/features/autocomplete) - Full autocomplete documentation
|
||||
- [CLI Documentation](/docs/code-with-ai/platforms/cli) - Complete CLI reference
|
||||
|
||||
@@ -5,6 +5,8 @@ description: "Connect your KiloClaw agent to Slack"
|
||||
|
||||
# Slack
|
||||
|
||||
{% youtube url="https://youtu.be/Q5bt-qH-_pY" title="Slack Setup Guide" caption="How to connect your KiloClaw agent to Slack" /%}
|
||||
|
||||
Connect your KiloClaw agent to Slack by creating a Slack app from the OpenClaw manifest and linking it to your KiloClaw dashboard.
|
||||
|
||||
## Step 1: Create a Slack App from the OpenClaw Manifest
|
||||
|
||||
@@ -5,6 +5,8 @@ description: "Connect your KiloClaw agent to Telegram"
|
||||
|
||||
# Telegram
|
||||
|
||||
{% youtube url="https://youtu.be/hIfKz073hGw" title="Telegram Setup Guide" caption="How to connect your KiloClaw agent to Telegram" /%}
|
||||
|
||||
Connect your KiloClaw agent to Telegram by creating a bot via BotFather and linking it to your KiloClaw dashboard.
|
||||
|
||||
1. Open Telegram and search for [@BotFather](https://t.me/BotFather)
|
||||
|
||||
@@ -42,6 +42,8 @@ Before you begin, make sure you have:
|
||||
|
||||
## Setup
|
||||
|
||||
{% youtube url="https://youtu.be/PX444_j3O4I" title="Google Workspace Setup Guide" caption="How to connect your Google account to KiloClaw" /%}
|
||||
|
||||
1. Go to the **Settings** tab on your [KiloClaw dashboard](/docs/kiloclaw/dashboard)
|
||||
2. Find the **Google Account** section
|
||||
3. Copy the provided `docker run` command — it includes a short-lived authentication token
|
||||
|
||||
@@ -25,3 +25,45 @@ For security reasons, SSH access is currently disabled for all KiloClaw instance
|
||||
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).
|
||||
|
||||
## How do I migrate my OpenClaw?
|
||||
|
||||
Whether you're migrating from another OpenClaw provider to KiloClaw, moving between KiloClaw instances (e.g., individual to org or vice versa), or leaving KiloClaw for another OpenClaw provider, you should plan to migrate your workspace, memory, and context so your new Claw retains the same knowledge as before.
|
||||
|
||||
You should plan to reconfigure integrations in the new instance as these are often tied to the instance and will break if you attempt migration.
|
||||
|
||||
### 1. Back up your workspace
|
||||
|
||||
Have your current instance export the workspace. We recommend creating a GitHub repo or `tar` archive file for easy loading.
|
||||
|
||||
If you are on KiloClaw, you can use
|
||||
|
||||
**GitHub export** — make sure [GitHub is configured](/docs/kiloclaw/development-tools/github) and ask your instance:
|
||||
|
||||
> Create a new GitHub repo and push your entire workspace there with the `gh` CLI. Tell me the URL of the repo you used.
|
||||
|
||||
**Google Drive** — make sure [Google Drive is configured](/docs/kiloclaw/development-tools/google) and ask your instance:
|
||||
|
||||
> Tar compress your workspace and push the file to Google Drive with the `gog` CLI. Then share the filename you used.
|
||||
|
||||
### 2. Stand up the new instance
|
||||
|
||||
### 3. Reconfigure integrations on the new instance
|
||||
|
||||
If you are using GitHub or Google Drive for the migration, prioritize that configuration.
|
||||
|
||||
### 4. Restore the workspace on the new instance
|
||||
|
||||
On your new Claw, restore the workspace from whichever backup method you used:
|
||||
|
||||
**From GitHub:**
|
||||
|
||||
> The GitHub repo `<repo>` has a backup of your workspace. Pull the workspace from the repo with the `gh` CLI and overwrite the existing workspace directory with the repo's contents.
|
||||
|
||||
**From Google Drive:**
|
||||
|
||||
> The Google Drive file `<filename>` has a backup of your workspace. Pull the tar file from Google Drive with the `gog` CLI and overwrite the existing workspace directory with its contents.
|
||||
|
||||
{% callout type="note" %}
|
||||
Replace `<repo>` or `<filename>` with the actual repository URL or filename from the backup step.
|
||||
{% /callout %}
|
||||
|
||||
@@ -7,26 +7,31 @@ description: "Automate your KiloClaw agent with webhooks and scheduled triggers"
|
||||
|
||||
Triggers let external events and schedules drive your KiloClaw agent automatically. Instead of typing every instruction yourself, triggers deliver messages to your agent on your behalf. This lets it react to real-world events or run tasks on a schedule without polling.
|
||||
|
||||
All triggers are managed from the **Settings** tab on your [KiloClaw dashboard](/docs/kiloclaw/dashboard).
|
||||
All triggers are managed from the **Settings** page in the KiloClaw section of the sidebar.
|
||||
|
||||
## Trigger Types
|
||||
|
||||
| Type | Description | Status |
|
||||
| --- | --- | --- |
|
||||
| [**Webhooks**](/docs/kiloclaw/triggers/webhooks) | Receive HTTP requests from external services (GitHub, Stripe, monitoring tools, etc.) and deliver them as chat messages to your agent | Available |
|
||||
| **Time-based** | Run tasks on a schedule using cron expressions | Coming soon |
|
||||
| Type | Description |
|
||||
| --- | --- |
|
||||
| [**Webhooks**](/docs/kiloclaw/triggers/webhooks) | Receive HTTP requests from external services (GitHub, Stripe, monitoring tools, etc.) and deliver them as chat messages to your agent |
|
||||
| [**Scheduled**](/docs/kiloclaw/triggers/scheduled) | Run tasks on a recurring schedule (e.g. every 15 minutes, daily at 9 AM, weekdays only) |
|
||||
|
||||
## How Triggers Work
|
||||
|
||||
1. An event occurs (an HTTP request arrives, or a schedule fires)
|
||||
2. The trigger validates the request and renders the payload through a **prompt template**
|
||||
3. The rendered message is delivered to your KiloClaw instance as a chat message
|
||||
1. A trigger fires
|
||||
2. Your **prompt template** is rendered into a message
|
||||
3. That message is delivered to your KiloClaw instance as a chat message
|
||||
4. Your agent processes and responds like any other conversation
|
||||
|
||||
Prompt templates give you control over how payloads are presented to your agent. You can include instructions, context, and formatting alongside the raw event data.
|
||||
Each trigger type has its own set of template variables. See the [Webhooks](/docs/kiloclaw/triggers/webhooks) and [Scheduled](/docs/kiloclaw/triggers/scheduled) pages for details.
|
||||
|
||||
{% callout type="warning" title="Triggers send prompts directly to your agent" %}
|
||||
When a trigger fires, the rendered message is sent directly to your KiloClaw agent as a prompt. If your instance is configured with a permission model that allows all actions, the agent will execute commands automatically without your explicit approval. This means triggers can cause your agent to take actions without you being aware. Review your instance's [permission settings](/docs/kiloclaw/control-ui/exec-approvals) and prompt templates carefully before enabling triggers.
|
||||
{% /callout %}
|
||||
|
||||
## Related
|
||||
|
||||
- [Webhooks](/docs/kiloclaw/triggers/webhooks)
|
||||
- [Scheduled Triggers](/docs/kiloclaw/triggers/scheduled)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Scheduled Triggers"
|
||||
description: "Run tasks on a schedule using cron expressions"
|
||||
---
|
||||
|
||||
# Scheduled Triggers
|
||||
|
||||
Scheduled triggers let your KiloClaw agent run tasks automatically on a recurring schedule. Instead of waiting for an external event, a scheduled trigger fires at the times you define using cron expressions. When it fires, the prompt template is rendered and delivered as a chat message to your KiloClaw instance, just like a webhook.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Go to **Settings** under the KiloClaw section in the sidebar
|
||||
2. Find the **Scheduled Triggers** section and click **Add Scheduled Trigger**
|
||||
3. Give your trigger a name (minimum 8 characters)
|
||||
4. Configure the schedule and prompt template
|
||||
5. Click **Save**
|
||||
|
||||
Each KiloClaw instance supports up to **5 scheduled triggers** alongside its single webhook.
|
||||
|
||||
## Configuring a Schedule
|
||||
|
||||
The schedule builder defaults to a friendly picker view. For more control, click **<> Advanced** to switch to raw cron input.
|
||||
|
||||
### Simple Mode (default)
|
||||
|
||||
Pick a frequency, time, and (optionally) days of the week from dropdown menus. The builder generates the cron expression for you behind the scenes and shows a preview of the next 5 upcoming runs.
|
||||
|
||||
- **Repeat**: Every 10 minutes, every 15 minutes, every 30 minutes, hourly, daily, weekly
|
||||
- **At**: Select the time of day (for daily and weekly frequencies)
|
||||
- **Day of week**: Select which days the trigger should fire (for weekly frequency)
|
||||
|
||||
### Advanced Mode
|
||||
|
||||
Click **<> Advanced** to enter a raw cron expression directly. This gives you full control over the schedule. The expression is validated in real time with a preview of upcoming fire times.
|
||||
|
||||
Cron expressions use the standard five-field format:
|
||||
|
||||
```
|
||||
┌───────── minute (0-59)
|
||||
│ ┌───────── hour (0-23)
|
||||
│ │ ┌───────── day of month (1-31)
|
||||
│ │ │ ┌───────── month (1-12)
|
||||
│ │ │ │ ┌───────── day of week (0-7, where 0 and 7 are Sunday)
|
||||
│ │ │ │ │
|
||||
* * * * *
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Expression | Meaning |
|
||||
| --- | --- |
|
||||
| `*/15 * * * *` | Every 15 minutes |
|
||||
| `0 9 * * 1-5` | 9:00 AM on weekdays |
|
||||
| `0 0 1 * *` | Midnight on the first of each month |
|
||||
| `30 14 * * 3` | 2:30 PM every Wednesday |
|
||||
|
||||
{% callout type="note" title="Minimum interval" %}
|
||||
The minimum interval between scheduled trigger runs is 10 minutes. Schedules more frequent than that are rejected.
|
||||
{% /callout %}
|
||||
|
||||
### Timezone
|
||||
|
||||
Select a timezone for your schedule. The default is UTC. All fire times are calculated relative to the selected timezone, including automatic handling of daylight saving time transitions.
|
||||
|
||||
## Prompt Template
|
||||
|
||||
The prompt template controls what message your agent receives when the schedule fires. You can customize it from the trigger's settings.
|
||||
|
||||
**Default template:**
|
||||
|
||||
```
|
||||
Run your scheduled task. Triggered at {{scheduledTime}}.
|
||||
```
|
||||
|
||||
**Available variables:**
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `{{scheduledTime}}` | The time the schedule fired (ISO string) |
|
||||
| `{{timestamp}}` | Capture timestamp (ISO string) |
|
||||
|
||||
{% callout type="note" title="Webhook variables are not available" %}
|
||||
Since scheduled triggers do not receive an HTTP request, variables like `{{body}}`, `{{bodyJson}}`, `{{headers}}`, `{{method}}`, `{{path}}`, and `{{query}}` are not populated. Use `{{scheduledTime}}` and `{{timestamp}}` instead.
|
||||
{% /callout %}
|
||||
|
||||
## Managing Scheduled Triggers
|
||||
|
||||
### Pause and Resume
|
||||
|
||||
Toggle the **Active/Paused** switch to temporarily stop a trigger from firing. When paused, the schedule is suspended but the configuration is preserved. Resume at any time to restart the schedule.
|
||||
|
||||
### Edit
|
||||
|
||||
You can update the cron expression, timezone, and prompt template of an existing scheduled trigger at any time. The activation mode (webhook vs. scheduled) cannot be changed after creation.
|
||||
|
||||
### Delete
|
||||
|
||||
Remove a scheduled trigger from the inline controls in the Settings panel. A confirmation dialog is shown before deletion.
|
||||
|
||||
## Viewing Scheduled Trigger Activity
|
||||
|
||||
Scheduled trigger invocations appear in the same request history as webhooks. The **Source** column shows a **Scheduled** badge to distinguish them from webhook-triggered requests. Click into a request to see the scheduled fire time and other details.
|
||||
|
||||
## Example: Daily Standup Summary
|
||||
|
||||
Create a scheduled trigger that fires every weekday morning and asks your agent to summarize overnight activity:
|
||||
|
||||
1. Add a scheduled trigger in your KiloClaw Settings
|
||||
2. Set the frequency to **Weekly** on **Monday through Friday** at **9:00 AM** in your local timezone
|
||||
3. Customize the prompt template:
|
||||
|
||||
```
|
||||
Good morning! Please summarize any overnight activity in the #engineering Slack channel and list open pull requests that need review today. Triggered at {{scheduledTime}}.
|
||||
```
|
||||
|
||||
Your agent will receive this message every weekday at 9:00 AM and respond with the summary.
|
||||
|
||||
## Related
|
||||
|
||||
- [Webhooks](/docs/kiloclaw/triggers/webhooks)
|
||||
- [Triggers Overview](/docs/kiloclaw/triggers)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
@@ -9,7 +9,7 @@ KiloClaw supports inbound webhooks so external events can trigger your agent aut
|
||||
|
||||
## Setup
|
||||
|
||||
1. Go to the **Settings** tab on your [KiloClaw dashboard](/docs/kiloclaw/dashboard)
|
||||
1. Go to **Settings** under the KiloClaw section in the sidebar
|
||||
2. Find the **Webhook Integration** card and click **Manage**
|
||||
3. Click **Set Up Webhook**
|
||||
|
||||
@@ -104,6 +104,8 @@ Now every push to that repository sends a payload to your agent. Customize the p
|
||||
|
||||
## Related
|
||||
|
||||
- [Scheduled Triggers](/docs/kiloclaw/triggers/scheduled)
|
||||
- [Triggers Overview](/docs/kiloclaw/triggers)
|
||||
- [KiloClaw Overview](/docs/kiloclaw/overview)
|
||||
- [Dashboard Reference](/docs/kiloclaw/dashboard)
|
||||
- [GitHub Integration](/docs/kiloclaw/development-tools/github)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 79 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 80 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.apertis.ai/v1>
|
||||
<!-- packages/opencode/src/provider/model-cache.ts -->
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx -->
|
||||
- <https://app.kilo.ai/usage>
|
||||
<!-- packages/opencode/src/kilocode/components/dialog-kilo-profile.tsx -->
|
||||
- <https://blog.kilo.ai/we-completely-rebuilt-the-kilo-vs-code-extension>
|
||||
- <https://blog.kilo.ai/p/new-kilo-for-vs-code-is-live>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx -->
|
||||
- <https://chatgpt.com/explore/plus>
|
||||
<!-- packages/opencode/src/provider/error.ts -->
|
||||
@@ -83,6 +83,8 @@
|
||||
- <https://kilo.ai/docs>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts -->
|
||||
<!-- packages/opencode/src/cli/cmd/tui/app.tsx -->
|
||||
- <https://kilo.ai/docs/code-with-ai/platforms/vscode/whats-new>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx -->
|
||||
- <https://kilo.ai/docs/providers/#custom-provider>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx -->
|
||||
- <https://kilo.ai/gateway>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-js": "catalog:",
|
||||
"@opentui/core": "0.1.75",
|
||||
"@opentui/solid": "0.1.75"
|
||||
},
|
||||
|
||||
@@ -27,8 +27,8 @@ export const DEFAULT_FREE_MODEL = "kilo-auto/free"
|
||||
/** Token expiration duration in milliseconds (1 year) */
|
||||
export const TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
/** User-Agent header value for requests */
|
||||
export const USER_AGENT = "opencode-kilo-provider"
|
||||
/** User-Agent header base value for requests */
|
||||
export const USER_AGENT_BASE = "opencode-kilo-provider"
|
||||
|
||||
/** Content-Type header value for requests */
|
||||
export const CONTENT_TYPE = "application/json"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { getKiloUrlFromToken } from "../auth/token.js"
|
||||
import { DEFAULT_HEADERS } from "../headers.js"
|
||||
import { getDefaultHeaders } from "../headers.js"
|
||||
import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS, PROMPTS, AI_SDK_PROVIDERS } from "./constants.js"
|
||||
|
||||
/**
|
||||
@@ -88,7 +88,7 @@ export async function fetchKiloModels(options?: {
|
||||
// Fetch models with timeout
|
||||
const response = await fetch(modelsURL, {
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
...getDefaultHeaders(),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { KILO_API_BASE, MODELS_FETCH_TIMEOUT_MS } from "./constants.js"
|
||||
import { DEFAULT_HEADERS } from "../headers.js"
|
||||
import { getDefaultHeaders } from "../headers.js"
|
||||
|
||||
/**
|
||||
* Group entry in an organization mode config.
|
||||
@@ -70,7 +70,7 @@ export async function fetchOrganizationModes(token: string, organizationId: stri
|
||||
const url = `${KILO_API_BASE}/api/organizations/${encodeURIComponent(organizationId)}/modes`
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
...getDefaultHeaders(),
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
HEADER_EDITORNAME,
|
||||
HEADER_MACHINEID,
|
||||
HEADER_FEATURE,
|
||||
USER_AGENT,
|
||||
USER_AGENT_BASE,
|
||||
CONTENT_TYPE,
|
||||
DEFAULT_EDITOR_NAME,
|
||||
ENV_EDITOR_NAME,
|
||||
@@ -36,12 +36,23 @@ export function getFeatureHeader(): string | undefined {
|
||||
return process.env[ENV_FEATURE] || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User-Agent header value.
|
||||
* Appends the version from KILOCODE_VERSION when available.
|
||||
*/
|
||||
export function getUserAgent(): string {
|
||||
const version = process.env[ENV_VERSION]
|
||||
return version ? `${USER_AGENT_BASE}/${version}` : USER_AGENT_BASE
|
||||
}
|
||||
|
||||
/**
|
||||
* Default headers for KiloCode requests
|
||||
*/
|
||||
export const DEFAULT_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Content-Type": CONTENT_TYPE,
|
||||
export function getDefaultHeaders(): Record<string, string> {
|
||||
return {
|
||||
"User-Agent": getUserAgent(),
|
||||
"Content-Type": CONTENT_TYPE,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ export { KiloAuthPlugin, default } from "./plugin.js"
|
||||
export { createKilo } from "./provider.js"
|
||||
export { createKiloDebug } from "./provider-debug.js"
|
||||
export { kiloCustomLoader } from "./loader.js"
|
||||
export { buildKiloHeaders, getEditorNameHeader, getFeatureHeader, DEFAULT_HEADERS } from "./headers.js"
|
||||
export { buildKiloHeaders, getEditorNameHeader, getFeatureHeader, getDefaultHeaders, getUserAgent } from "./headers.js"
|
||||
|
||||
// ============================================================================
|
||||
// Auth
|
||||
@@ -86,7 +86,7 @@ export {
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_FREE_MODEL,
|
||||
TOKEN_EXPIRATION_MS,
|
||||
USER_AGENT,
|
||||
USER_AGENT_BASE,
|
||||
CONTENT_TYPE,
|
||||
DEFAULT_PROVIDER_NAME,
|
||||
ANONYMOUS_API_KEY,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import type { Provider as SDK } from "ai"
|
||||
import type { KiloProviderOptions } from "./types.js"
|
||||
import { getKiloUrlFromToken, getApiKey } from "./auth/token.js"
|
||||
import { buildKiloHeaders, DEFAULT_HEADERS } from "./headers.js"
|
||||
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
|
||||
import { KILO_API_BASE, ANONYMOUS_API_KEY } from "./api/constants.js"
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ export function createKiloDebug(options: KiloProviderOptions = {}): SDK {
|
||||
|
||||
// Merge custom headers with defaults
|
||||
const customHeaders = {
|
||||
...DEFAULT_HEADERS,
|
||||
...getDefaultHeaders(),
|
||||
...buildKiloHeaders(undefined, {
|
||||
kilocodeOrganizationId: options.kilocodeOrganizationId,
|
||||
kilocodeTesterWarningsDisabledUntil: undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import type { KiloProvider, KiloProviderOptions } from "./types.js"
|
||||
import { getKiloUrlFromToken, getApiKey } from "./auth/token.js"
|
||||
import { buildKiloHeaders, DEFAULT_HEADERS } from "./headers.js"
|
||||
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
|
||||
import { KILO_API_BASE, ANONYMOUS_API_KEY } from "./api/constants.js"
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
|
||||
|
||||
// Merge custom headers with defaults
|
||||
const customHeaders = {
|
||||
...DEFAULT_HEADERS,
|
||||
...getDefaultHeaders(),
|
||||
...buildKiloHeaders(undefined, {
|
||||
kilocodeOrganizationId: options.kilocodeOrganizationId,
|
||||
kilocodeTesterWarningsDisabledUntil: undefined,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { fetchProfile, fetchBalance } from "../api/profile.js"
|
||||
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
|
||||
import { fetchOrganizationModes, clearModesCache } from "../api/modes.js"
|
||||
import { KILO_API_BASE, HEADER_FEATURE } from "../api/constants.js"
|
||||
import { KILO_API_BASE, HEADER_FEATURE, HEADER_ORGANIZATIONID } from "../api/constants.js"
|
||||
import { buildKiloHeaders } from "../headers.js"
|
||||
import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js"
|
||||
import { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "../cloud-sessions.js"
|
||||
@@ -536,12 +536,16 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
|
||||
if (!token) return c.json({ error: "No valid token found" }, 401)
|
||||
|
||||
const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
const organizationId = auth.type === "oauth" ? auth.accountId : undefined
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if (organizationId) {
|
||||
headers[HEADER_ORGANIZATIONID] = organizationId
|
||||
}
|
||||
|
||||
const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers })
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
@@ -589,12 +593,16 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
|
||||
if (!token) return c.json({ error: "No valid token found" }, 401)
|
||||
|
||||
const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/chat-credentials`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
const organizationId = auth.type === "oauth" ? auth.accountId : undefined
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if (organizationId) {
|
||||
headers[HEADER_ORGANIZATIONID] = organizationId
|
||||
}
|
||||
|
||||
const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/chat-credentials`, { headers })
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.gradle/
|
||||
build/
|
||||
.intellijPlatform/
|
||||
.ai/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -0,0 +1,71 @@
|
||||
# Kilo JetBrains
|
||||
|
||||
AI coding agent plugin for JetBrains IDEs.
|
||||
|
||||
---
|
||||
|
||||
## Set up your environment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Bun** -- used to build CLI binaries and run build scripts
|
||||
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK
|
||||
- **IntelliJ IDEA** -- to run the plugin in a sandboxed IDE
|
||||
|
||||
---
|
||||
|
||||
## Open in IntelliJ
|
||||
|
||||
When you open the monorepo root in IntelliJ IDEA, the Gradle project at `packages/kilo-jetbrains/` should be auto-detected via `.idea/gradle.xml`. If not, link it manually: **File > Settings > Build Tools > Gradle > +** and select `packages/kilo-jetbrains/settings.gradle.kts`.
|
||||
|
||||
---
|
||||
|
||||
## Build locally
|
||||
|
||||
From `packages/kilo-jetbrains/`:
|
||||
|
||||
```
|
||||
bun run build
|
||||
```
|
||||
|
||||
This builds the CLI binary for your current OS/arch only, copies it into the backend module resources, and runs `./gradlew buildPlugin`. The plugin archive is output to `build/distributions/`.
|
||||
|
||||
Or via Turbo from the repo root:
|
||||
|
||||
```
|
||||
bun turbo build --filter=@kilocode/kilo-jetbrains
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build for production
|
||||
|
||||
From `packages/kilo-jetbrains/`:
|
||||
|
||||
```
|
||||
bun run build:production
|
||||
```
|
||||
|
||||
This builds CLI binaries for all 6 desktop platforms (darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64, windows-arm64), copies them all into the backend jar, and fails if any are missing. Gradle also validates all platforms are present via `-Pproduction=true`.
|
||||
|
||||
The built plugin archive is at `build/distributions/kilo.jetbrains-<version>.zip`. This zip can be installed in any JetBrains IDE via **Settings > Plugins > Install Plugin from Disk**.
|
||||
|
||||
---
|
||||
|
||||
## Run the plugin
|
||||
|
||||
After building, use the `runIde` Gradle task (available in the Gradle tool window or via the "Run JetBrains Plugin" run configuration) to launch a sandboxed IntelliJ instance with the plugin installed.
|
||||
|
||||
Note: `runIde` does not build CLI binaries -- run `bun run build` at least once before using it. Subsequent Kotlin/Gradle changes can be iterated with `runIde` directly.
|
||||
|
||||
---
|
||||
|
||||
## Run Gradle directly
|
||||
|
||||
You can run `./gradlew buildPlugin` directly if CLI binaries are already in `backend/build/generated/cli/`. Gradle will fail with a clear error if they are missing.
|
||||
|
||||
For production verification:
|
||||
|
||||
```
|
||||
./gradlew buildPlugin -Pproduction=true
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources.srcDir(layout.buildDirectory.dir("generated/cli"))
|
||||
}
|
||||
}
|
||||
|
||||
val cliDir = layout.buildDirectory.dir("generated/cli/cli")
|
||||
val production = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false)
|
||||
|
||||
val requiredPlatforms = listOf(
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-arm64",
|
||||
"linux-x64",
|
||||
"windows-x64",
|
||||
"windows-arm64",
|
||||
)
|
||||
|
||||
val checkCli by tasks.registering {
|
||||
description = "Verify CLI binaries exist before building"
|
||||
val dir = cliDir.map { it.asFile }
|
||||
val prod = production.get()
|
||||
val platforms = requiredPlatforms.toList()
|
||||
doLast {
|
||||
val resolved = dir.get()
|
||||
if (!resolved.exists() || resolved.listFiles()?.isEmpty() != false) {
|
||||
throw GradleException(
|
||||
"CLI binaries not found at ${resolved.absolutePath}.\n" +
|
||||
"Run 'bun run build' from packages/kilo-jetbrains/ to build CLI and plugin together."
|
||||
)
|
||||
}
|
||||
if (prod) {
|
||||
val missing = platforms.filter { platform ->
|
||||
val dir = File(resolved, platform)
|
||||
val exe = if (platform.startsWith("windows")) "kilo.exe" else "kilo"
|
||||
!File(dir, exe).exists()
|
||||
}
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"Production build requires all platform CLI binaries.\n" +
|
||||
"Missing: ${missing.joinToString(", ")}\n" +
|
||||
"Run 'bun run build:production' to build all platforms."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(checkCli)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
intellijIdea(libs.versions.intellij.platform)
|
||||
bundledModule("intellij.platform.backend")
|
||||
}
|
||||
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<idea-plugin>
|
||||
<dependencies>
|
||||
<module name="intellij.platform.backend"/>
|
||||
<module name="kilo.jetbrains.shared"/>
|
||||
</dependencies>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,60 @@
|
||||
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
|
||||
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
|
||||
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget
|
||||
|
||||
group = "ai.kilocode.jetbrains"
|
||||
version = "7.0.1"
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("java")
|
||||
alias(libs.plugins.intellij.platform)
|
||||
|
||||
alias(libs.plugins.kotlin) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.intellij.platform.module")
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
intellijPlatform {
|
||||
defaultRepositories()
|
||||
}
|
||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
intellijIdea(libs.versions.intellij.platform)
|
||||
|
||||
pluginModule(implementation(project(":shared")))
|
||||
pluginModule(implementation(project(":frontend")))
|
||||
pluginModule(implementation(project(":backend")))
|
||||
testFramework(TestFrameworkType.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
intellijPlatform {
|
||||
splitMode = true
|
||||
splitModeTarget = SplitModeTarget.BOTH
|
||||
|
||||
pluginVerification {
|
||||
ides {
|
||||
create(IntelliJPlatformType.IntellijIdeaCommunity, libs.versions.intellij.platform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<JavaExec>("runIde") {
|
||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||
listOf("-Dnosplash=true")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
intellijIdea(libs.versions.intellij.platform)
|
||||
bundledModule("intellij.platform.frontend")
|
||||
}
|
||||
|
||||
implementation(project(":shared"))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<idea-plugin>
|
||||
<dependencies>
|
||||
<module name="intellij.platform.frontend"/>
|
||||
<module name="kilo.jetbrains.shared"/>
|
||||
</dependencies>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,4 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
|
||||
@@ -0,0 +1,12 @@
|
||||
[versions]
|
||||
intellij-platform = "2025.3"
|
||||
intellij-gradle-plugin = "2.10.5"
|
||||
kotlin-jvm-plugin = "2.1.20"
|
||||
kotlin-serialization-plugin = "2.1.20"
|
||||
kotlin-serialization = "1.7.3"
|
||||
|
||||
[plugins]
|
||||
intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-gradle-plugin" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-jvm-plugin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization-plugin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-jvm-plugin" }
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "bun script/build.ts",
|
||||
"build:production": "bun script/build.ts --production"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Build the Kilo JetBrains plugin.
|
||||
*
|
||||
* Usage:
|
||||
* bun script/build.ts # Local build — only current platform binary required
|
||||
* bun script/build.ts --production # Production build — all 6 platform binaries required
|
||||
*
|
||||
* Steps:
|
||||
* 1. Builds CLI binaries (or uses prebuilt ones from dist/).
|
||||
* Local: builds only current platform (--single).
|
||||
* Production: builds all platforms.
|
||||
* 2. Copies them into backend/build/generated/cli/cli/{os}/kilo[.exe]
|
||||
* so they end up inside the backend jar at /cli/{os}/kilo.
|
||||
* 3. Invokes Gradle to build the plugin.
|
||||
*/
|
||||
|
||||
import { $ } from "bun"
|
||||
import { join, relative } from "node:path"
|
||||
import { existsSync, mkdirSync, chmodSync, cpSync, rmSync } from "node:fs"
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
|
||||
const root = join(import.meta.dir, "..")
|
||||
const packages = join(root, "..")
|
||||
const opencodeDir = join(packages, "opencode")
|
||||
const distDir = join(opencodeDir, "dist")
|
||||
const cliDir = join(root, "backend", "build", "generated", "cli", "cli")
|
||||
|
||||
/** All desktop platforms. */
|
||||
const platforms = [
|
||||
{ os: "darwin-arm64", exe: "kilo" },
|
||||
{ os: "darwin-x64", exe: "kilo" },
|
||||
{ os: "linux-arm64", exe: "kilo" },
|
||||
{ os: "linux-x64", exe: "kilo" },
|
||||
{ os: "windows-x64", exe: "kilo.exe" },
|
||||
{ os: "windows-arm64", exe: "kilo.exe" },
|
||||
] as const
|
||||
|
||||
function localPlatformTag(): string {
|
||||
const os = process.platform === "win32" ? "windows" : process.platform
|
||||
return `${os}-${process.arch}`
|
||||
}
|
||||
|
||||
function log(msg: string) {
|
||||
console.log(`[jetbrains-build] ${msg}`)
|
||||
}
|
||||
|
||||
function distBinPath(os: string, exe: string): string {
|
||||
return join(distDir, `@kilocode/cli-${os}`, "bin", exe)
|
||||
}
|
||||
|
||||
function hasDist(): boolean {
|
||||
if (production) {
|
||||
return platforms.every((p) => existsSync(distBinPath(p.os, p.exe)))
|
||||
}
|
||||
const tag = localPlatformTag()
|
||||
const local = platforms.find((p) => p.os === tag)
|
||||
return local ? existsSync(distBinPath(local.os, local.exe)) : false
|
||||
}
|
||||
|
||||
async function prepareCli() {
|
||||
const mode = production ? "production" : "local"
|
||||
log(`Mode: ${mode}`)
|
||||
|
||||
if (!hasDist()) {
|
||||
log("Building CLI binaries via opencode...")
|
||||
if (!existsSync(join(opencodeDir, "package.json"))) {
|
||||
throw new Error(`Expected opencode package at ${opencodeDir}`)
|
||||
}
|
||||
const args = production ? [] : ["--single"]
|
||||
await $`bun run build ${args}`.cwd(opencodeDir)
|
||||
} else {
|
||||
log("Found prebuilt CLI binaries in opencode/dist/, skipping CLI build")
|
||||
}
|
||||
|
||||
if (existsSync(cliDir)) {
|
||||
rmSync(cliDir, { recursive: true })
|
||||
}
|
||||
|
||||
const missing: string[] = []
|
||||
let copied = 0
|
||||
for (const p of platforms) {
|
||||
const src = distBinPath(p.os, p.exe)
|
||||
if (!existsSync(src)) {
|
||||
missing.push(p.os)
|
||||
continue
|
||||
}
|
||||
|
||||
const dir = join(cliDir, p.os)
|
||||
mkdirSync(dir, { recursive: true })
|
||||
const dest = join(dir, p.exe)
|
||||
cpSync(src, dest)
|
||||
chmodSync(dest, 0o755)
|
||||
copied++
|
||||
log(`Copied ${relative(root, src)} -> ${relative(root, dest)}`)
|
||||
}
|
||||
|
||||
if (copied === 0) {
|
||||
throw new Error("No CLI binaries were copied — the build cannot proceed")
|
||||
}
|
||||
|
||||
if (production && missing.length > 0) {
|
||||
throw new Error(`Production build requires all platform binaries. Missing: ${missing.join(", ")}`)
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
log(`Skipped ${missing.length} platforms (not needed for local build): ${missing.join(", ")}`)
|
||||
}
|
||||
|
||||
log(`Copied ${copied}/${platforms.length} platform binaries`)
|
||||
}
|
||||
|
||||
const gradlew = process.platform === "win32" ? "gradlew.bat" : "./gradlew"
|
||||
|
||||
async function buildPlugin() {
|
||||
log("Building JetBrains plugin via Gradle...")
|
||||
const args = production ? ["-Pproduction=true"] : []
|
||||
await $`${gradlew} buildPlugin ${args}`.cwd(root)
|
||||
log("Done. Plugin archive is in build/distributions/")
|
||||
}
|
||||
|
||||
try {
|
||||
await prepareCli()
|
||||
await buildPlugin()
|
||||
} catch (err) {
|
||||
console.error(`[jetbrains-build] ERROR: ${err instanceof Error ? err.message : String(err)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
rootProject.name = "kilo.jetbrains"
|
||||
|
||||
include("shared")
|
||||
include("frontend")
|
||||
include("backend")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
intellijIdea(libs.versions.intellij.platform)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<idea-plugin>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,17 @@
|
||||
<idea-plugin>
|
||||
<name>Kilo Code</name>
|
||||
<vendor url="https://kilo.ai">Kilo Code</vendor>
|
||||
|
||||
<description><![CDATA[
|
||||
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.
|
||||
]]></description>
|
||||
|
||||
<content>
|
||||
<module name="kilo.jetbrains.shared" loading="required"/>
|
||||
<module name="kilo.jetbrains.frontend"/>
|
||||
<module name="kilo.jetbrains.backend"/>
|
||||
</content>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 0H0V512H512V0Z" fill="black"/>
|
||||
<path d="M512 512H0V0H512V512ZM322.783 322.784H278.261V392.747L308.472 422.958H378.435V378.437H322.782L322.783 322.784ZM422.957 308.474L392.746 278.263H322.783V322.784H378.435L378.435 378.437H422.957L422.957 308.474ZM233.739 278.263H189.217V322.784H233.739V278.263ZM89.0435 392.747L119.254 422.958H233.739V378.437H133.565V278.263H89.043L89.0435 392.747ZM372.538 189.217V119.254L342.327 89.0435H278.261V133.565H328.017V189.217H278.261V233.739H422.957V189.217H372.538ZM133.565 89.0435H89.0435V233.739H133.565V183.652H189.218V233.739H233.74V183.652L189.218 139.13H133.565V89.0435ZM233.739 89.0435H189.217L189.218 139.13H233.739V89.0435Z" fill="#FAF74F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.1.19",
|
||||
"version": "7.1.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
@@ -102,7 +102,7 @@
|
||||
"storybook": "10.2.10",
|
||||
"storybook-solidjs-vite": "10.0.9",
|
||||
"typescript": "catalog:",
|
||||
"vite": "7.3.1",
|
||||
"vite": "7.3.2",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -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.1.19",
|
||||
"version": "7.1.22",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
@@ -602,12 +602,14 @@
|
||||
{
|
||||
"command": "kilo-code.new.cycleAgentMode",
|
||||
"key": "ctrl+.",
|
||||
"mac": "cmd+."
|
||||
"mac": "cmd+.",
|
||||
"when": "sideBarFocus && kilo-code.new.sidebarVisible || activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' || activeWebviewPanelId == 'kilo-code.new.TabPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.cyclePreviousAgentMode",
|
||||
"key": "ctrl+shift+.",
|
||||
"mac": "cmd+shift+."
|
||||
"mac": "cmd+shift+.",
|
||||
"when": "sideBarFocus && kilo-code.new.sidebarVisible || activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' || activeWebviewPanelId == 'kilo-code.new.TabPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.autocomplete.cancelSuggestions",
|
||||
@@ -704,6 +706,11 @@
|
||||
"default": false,
|
||||
"description": "Enable chat textarea autocomplete"
|
||||
},
|
||||
"kilo-code.new.claudeCodeCompat": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Load CLAUDE.md instructions and skills from your Claude Code configuration directory into Kilo sessions. Enable this if you want Kilo to use your Claude Code instructions and skills."
|
||||
},
|
||||
"kilo-code.new.browserAutomation.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -760,6 +767,11 @@
|
||||
"none"
|
||||
],
|
||||
"description": "Sound to play on errors"
|
||||
},
|
||||
"kilo-code.new.showTaskTimeline": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show the task timeline graph in the chat header"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,7 +830,7 @@
|
||||
"ts-morph": "27.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "7.3.1",
|
||||
"vite": "7.3.2",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -828,7 +840,7 @@
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "^7.0.0",
|
||||
"diff": "8.0.4",
|
||||
"dotenv": "^16.4.7",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"friendly-words": "1.3.1",
|
||||
@@ -837,11 +849,11 @@
|
||||
"lru-cache": "^11.0.2",
|
||||
"openai": "^4.85.4",
|
||||
"quick-lru": "^7.0.0",
|
||||
"simple-git": "3.31.1",
|
||||
"simple-git": "3.35.2",
|
||||
"solid-js": "^1.9.11",
|
||||
"uri-js": "^4.4.1",
|
||||
"web-tree-sitter": "^0.24.7",
|
||||
"yaml": "2.8.2",
|
||||
"yaml": "2.8.3",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { existsSync, mkdirSync, rmSync, chmodSync } from "node:fs"
|
||||
const packageJsonPath = join(import.meta.dir, "..", "package.json")
|
||||
const packageJson = await Bun.file(packageJsonPath).json()
|
||||
const version = process.env.KILO_VERSION ? process.env.KILO_VERSION : packageJson.version
|
||||
const prerelease = process.env.KILO_PRE_RELEASE === "true"
|
||||
|
||||
console.log(`Building VSCode extension version: ${version}`)
|
||||
console.log(`Building VSCode extension version: ${version}${prerelease ? " (pre-release)" : ""}`)
|
||||
|
||||
if (packageJson.version !== version) {
|
||||
console.log(`Updating package.json version from ${packageJson.version} to ${version}`)
|
||||
@@ -80,9 +81,11 @@ for (const config of targets) {
|
||||
|
||||
console.log(` ✅ Binary ready at ${targetBinary}`)
|
||||
|
||||
console.log(` 📦 Packaging .vsix for ${config.target}...`)
|
||||
console.log(` 📦 Packaging .vsix for ${config.target}${prerelease ? " (pre-release)" : ""}...`)
|
||||
const vsixPath = join(outDir, `kilo-vscode-${config.target}.vsix`)
|
||||
await $`vsce package --no-dependencies --skip-license --target ${config.target} -o ${vsixPath}`.env({
|
||||
const args = ["--no-dependencies", "--skip-license", "--target", config.target, "-o", vsixPath]
|
||||
if (prerelease) args.push("--pre-release")
|
||||
await $`vsce package ${args}`.env({
|
||||
...process.env,
|
||||
npm_config_ignore_scripts: "true",
|
||||
})
|
||||
|
||||
@@ -4,7 +4,9 @@ import { join } from "node:path"
|
||||
import { existsSync } from "node:fs"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
||||
console.log(`Publishing VSCode extension for release: v${Script.version}`)
|
||||
const prerelease = process.env.KILO_PRE_RELEASE === "true"
|
||||
|
||||
console.log(`Publishing VSCode extension for ${prerelease ? "pre-release" : "release"}: v${Script.version}`)
|
||||
|
||||
const outDir = process.env.VSIX_DIR || join(import.meta.dir, "..", "out")
|
||||
|
||||
@@ -36,14 +38,16 @@ for (const target of targets) {
|
||||
|
||||
console.log(`\nFound ${vsixFiles.length} VSIX files`)
|
||||
|
||||
const flag = prerelease ? ["--pre-release"] : []
|
||||
|
||||
for (const target of targets) {
|
||||
const vsixPath = join(outDir, `kilo-vscode-${target}.vsix`)
|
||||
console.log(`\n🚀 Publishing ${target} to VS Code Marketplace...`)
|
||||
await $`vsce publish --packagePath ${vsixPath}`
|
||||
console.log(`\n🚀 Publishing ${target} to VS Code Marketplace${prerelease ? " (pre-release)" : ""}...`)
|
||||
await $`vsce publish ${flag} --packagePath ${vsixPath}`
|
||||
console.log(` ✅ Published ${target} to VS Code Marketplace`)
|
||||
|
||||
console.log(`\n📤 Publishing ${target} to Open VSX...`)
|
||||
await $`npx ovsx publish --pat ${process.env.OPENVSX_TOKEN} --packagePath ${vsixPath}`
|
||||
console.log(`\n📤 Publishing ${target} to Open VSX${prerelease ? " (pre-release)" : ""}...`)
|
||||
await $`npx ovsx publish ${flag} --pat ${process.env.OPENVSX_TOKEN} --packagePath ${vsixPath}`
|
||||
console.log(` ✅ Published ${target} to Open VSX`)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
checkAndShowMigrationWizard,
|
||||
handleRequestLegacyMigrationData,
|
||||
handleStartLegacyMigration,
|
||||
handleFinalizeLegacyMigration,
|
||||
handleSkipLegacyMigration,
|
||||
handleClearLegacyData,
|
||||
type MigrationContext,
|
||||
@@ -68,7 +69,11 @@ import {
|
||||
fetchAndSendPendingPermissions,
|
||||
type PermissionContext,
|
||||
} from "./kilo-provider/handlers/permission-handler"
|
||||
import { handleQuestionReply, handleQuestionReject } from "./kilo-provider/handlers/question"
|
||||
import {
|
||||
handleQuestionReply,
|
||||
handleQuestionReject,
|
||||
fetchAndSendPendingQuestions,
|
||||
} from "./kilo-provider/handlers/question"
|
||||
|
||||
import {
|
||||
buildActionContext,
|
||||
@@ -331,8 +336,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Handle messages from webview (shared handler)
|
||||
this.setupWebviewMessageHandler(webviewView.webview)
|
||||
|
||||
// Pause stats polling when sidebar is hidden, resume when visible
|
||||
// Track sidebar visibility for keybinding when-clauses and stats polling
|
||||
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
|
||||
webviewView.onDidChangeVisibility(() => {
|
||||
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
|
||||
this.statsPoller?.setEnabled(webviewView.visible)
|
||||
})
|
||||
|
||||
@@ -786,9 +793,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "requestBrowserSettings":
|
||||
this.sendBrowserSettings()
|
||||
break
|
||||
case "requestClaudeCompatSetting":
|
||||
this.sendClaudeCompatSetting()
|
||||
break
|
||||
case "requestNotificationSettings":
|
||||
this.sendNotificationSettings()
|
||||
break
|
||||
case "requestTimelineSetting":
|
||||
this.sendTimelineSetting()
|
||||
break
|
||||
case "requestNotifications":
|
||||
this.fetchAndSendNotifications().catch((e) =>
|
||||
console.error("[Kilo New] fetchAndSendNotifications failed:", e),
|
||||
@@ -891,6 +904,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "clearLegacyData":
|
||||
void handleClearLegacyData(this.migrationCtx)
|
||||
break
|
||||
case "finalizeLegacyMigration":
|
||||
void handleFinalizeLegacyMigration(this.migrationCtx)
|
||||
break
|
||||
// legacy-migration end
|
||||
case "enhancePrompt": {
|
||||
const sdkClient = this.client
|
||||
@@ -1042,6 +1058,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
await this.syncWebviewState("sse-connected")
|
||||
await this.flushPendingSessionRefresh("sse-connected")
|
||||
await fetchAndSendPendingPermissions(this.permissionCtx)
|
||||
await fetchAndSendPendingQuestions(this.questionCtx)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Failed during connected state handling:", error)
|
||||
this.postMessage({
|
||||
@@ -1120,6 +1137,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.seedSessionStatusMap(),
|
||||
])
|
||||
this.sendNotificationSettings()
|
||||
this.sendTimelineSetting()
|
||||
|
||||
// Start polling worktree diff stats for the sidebar badge
|
||||
this.startStatsPolling()
|
||||
@@ -1326,6 +1344,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
sessionID,
|
||||
messages,
|
||||
})
|
||||
|
||||
// Recover any missed permission/question prompts emitted by the child before
|
||||
// we started tracking it. Both run fire-and-forget after messagesLoaded so
|
||||
// the webview isn't blocked.
|
||||
void fetchAndSendPendingPermissions(this.permissionCtx)
|
||||
void fetchAndSendPendingQuestions(this.questionCtx)
|
||||
} catch (err) {
|
||||
this.syncedChildSessions.delete(sessionID)
|
||||
console.error("[Kilo New] KiloProvider: Failed to sync child session:", err)
|
||||
@@ -2059,6 +2083,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
})
|
||||
}
|
||||
|
||||
private sendTimelineSetting(): void {
|
||||
const config = vscode.workspace.getConfiguration("kilo-code.new")
|
||||
this.postMessage({
|
||||
type: "timelineSettingLoaded",
|
||||
visible: config.get<boolean>("showTaskTimeline", true),
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns the number of sessions currently in "busy" state. */
|
||||
private getBusySessionCount(): number {
|
||||
return getBusySessionCount(this.sessionStatusMap)
|
||||
@@ -2390,6 +2422,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return {
|
||||
client: this.client,
|
||||
currentSessionId: this.currentSession?.id,
|
||||
trackedSessionIds: this.trackedSessionIds,
|
||||
sessionDirectories: this.sessionDirectories,
|
||||
postMessage: (msg: unknown) => this.postMessage(msg),
|
||||
getWorkspaceDirectory: (sid?: string) => this.getWorkspaceDirectory(sid),
|
||||
}
|
||||
@@ -2561,6 +2595,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.sendAutocompleteSettings()
|
||||
this.sendBrowserSettings()
|
||||
this.sendNotificationSettings()
|
||||
this.sendTimelineSetting()
|
||||
|
||||
// Re-send globalState items to the webview
|
||||
this.postMessage({ type: "variantsLoaded", variants: {} })
|
||||
@@ -2587,6 +2622,17 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current Claude Code compatibility setting and push it to the webview.
|
||||
*/
|
||||
private sendClaudeCompatSetting(): void {
|
||||
const enabled = vscode.workspace.getConfiguration("kilo-code.new").get<boolean>("claudeCodeCompat", false)
|
||||
this.postMessage({
|
||||
type: "claudeCompatSettingLoaded",
|
||||
enabled: enabled ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
/** Re-fetch all server-side state after an auth change. */
|
||||
private async reloadAfterAuthChange(): Promise<void> {
|
||||
await Promise.all([
|
||||
@@ -2672,6 +2718,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.contextSessionID = event.properties.info.id
|
||||
}
|
||||
|
||||
// Auto-adopt child sessions as soon as the task tool part reveals their ID.
|
||||
// This means the child's permission/question events are tracked immediately —
|
||||
// before the webview renderer has a chance to call syncSession — eliminating
|
||||
// the race where the child blocks on a prompt that the UI never sees.
|
||||
if (event.type === "message.part.updated") {
|
||||
const part = event.properties.part as {
|
||||
type?: string
|
||||
tool?: string
|
||||
metadata?: { sessionId?: string }
|
||||
sessionID?: string
|
||||
}
|
||||
const childId = part.type === "tool" && part.tool === "task" ? part.metadata?.sessionId : undefined
|
||||
if (childId && !this.trackedSessionIds.has(childId)) {
|
||||
console.log("[Kilo New] KiloProvider: 🔗 Auto-adopting child session from task tool", { childId })
|
||||
void this.handleSyncSession(childId, part.sessionID ?? sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
const msg = mapSSEEventToWebviewMessage(event, sessionID)
|
||||
if (msg) {
|
||||
if (msg.type === "partUpdated") {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { parseWorktreeList, normalizePath } from "./git-import"
|
||||
|
||||
interface GitOpsOptions {
|
||||
log: (...args: unknown[]) => void
|
||||
refreshMs?: number
|
||||
/** Override git command execution for testing. */
|
||||
runGit?: (args: string[], cwd: string) => Promise<string>
|
||||
}
|
||||
@@ -40,15 +39,31 @@ interface ExecResult {
|
||||
stderr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Build environment variables that prevent git and SSH from opening interactive
|
||||
* prompts. Used for background operations (e.g. periodic fetch) so users with
|
||||
* SSH keys that require passphrase confirmation are not bombarded with dialogs.
|
||||
*
|
||||
* Returns a full `process.env` overlay suitable for `simple-git.env()` or
|
||||
* `child_process.spawn`. `GIT_SSH_COMMAND` is only overridden when the user
|
||||
* hasn't already configured their own.
|
||||
*/
|
||||
export function nonInteractiveEnv(): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
}
|
||||
if (!process.env.GIT_SSH_COMMAND) {
|
||||
env.GIT_SSH_COMMAND = "ssh -o BatchMode=yes"
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
export class GitOps {
|
||||
private lastFetch = new Map<string, number>()
|
||||
private inflightFetch = new Map<string, Promise<void>>()
|
||||
private readonly refreshMs: number
|
||||
private readonly log: (...args: unknown[]) => void
|
||||
private readonly runGit: (args: string[], cwd: string) => Promise<string>
|
||||
|
||||
constructor(options: GitOpsOptions) {
|
||||
this.refreshMs = options.refreshMs ?? 120000
|
||||
this.log = options.log
|
||||
this.runGit =
|
||||
options.runGit ??
|
||||
@@ -114,33 +129,6 @@ export class GitOps {
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
async refreshRemote(cwd: string, remote: string): Promise<void> {
|
||||
if (!remote) return
|
||||
|
||||
const commonRaw = await this.raw(["rev-parse", "--git-common-dir"], cwd).catch(() => cwd)
|
||||
const common = nodePath.isAbsolute(commonRaw) ? commonRaw : nodePath.resolve(cwd, commonRaw)
|
||||
const key = `${common}:${remote}`
|
||||
|
||||
const existing = this.inflightFetch.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
const prev = this.lastFetch.get(key) ?? 0
|
||||
const now = Date.now()
|
||||
if (now - prev < this.refreshMs) return
|
||||
this.lastFetch.set(key, now)
|
||||
|
||||
const job = this.raw(["fetch", "--quiet", "--no-tags", remote], cwd)
|
||||
.catch((err) => {
|
||||
this.log(`Failed to refresh remote refs for ${cwd}:`, err)
|
||||
})
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
this.inflightFetch.delete(key)
|
||||
})
|
||||
this.inflightFetch.set(key, job)
|
||||
return job
|
||||
}
|
||||
|
||||
/** Return the set of worktree paths for the repo, excluding bare entries. */
|
||||
async listWorktreePaths(cwd: string): Promise<Set<string>> {
|
||||
const raw = await this.raw(["worktree", "list", "--porcelain"], cwd)
|
||||
@@ -211,12 +199,11 @@ export class GitOps {
|
||||
/**
|
||||
* Count commits ahead and behind using `rev-list --left-right --count`.
|
||||
* 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").
|
||||
* Counts are computed against local tracking refs only — no fetch is
|
||||
* performed, so values may be stale until an explicit git operation
|
||||
* (push, pull, etc.) updates the refs.
|
||||
*/
|
||||
async aheadBehind(cwd: string, base: string, remote?: string): Promise<{ ahead: number; behind: number }> {
|
||||
if (remote) await this.refreshRemote(cwd, remote)
|
||||
async aheadBehind(cwd: string, base: string): Promise<{ ahead: number; behind: number }> {
|
||||
return this.parseLeftRight(cwd, base)
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ export class GitStatsPoller {
|
||||
const base = remoteRef(wt)
|
||||
const [{ data: diffs }, ab] = await Promise.all([
|
||||
client.worktree.diffSummary({ directory: wt.path, base }, { throwOnError: true }),
|
||||
this.git.aheadBehind(wt.path, base, wt.remote),
|
||||
this.git.aheadBehind(wt.path, base),
|
||||
])
|
||||
const files = diffs.length
|
||||
const additions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.additions, 0)
|
||||
@@ -248,7 +248,6 @@ export class GitStatsPoller {
|
||||
|
||||
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
|
||||
@@ -260,7 +259,7 @@ export class GitStatsPoller {
|
||||
this.options.log(`Local stats: using HTTP client with base=${base}`)
|
||||
const [{ data: diffs }, ab] = await Promise.all([
|
||||
client.worktree.diffSummary({ directory: root, base }, { throwOnError: true }),
|
||||
this.git.aheadBehind(root, base, remote),
|
||||
this.git.aheadBehind(root, base),
|
||||
])
|
||||
files = diffs.length
|
||||
additions = diffs.reduce((sum: number, d: FileDiff) => sum + d.additions, 0)
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as fs from "fs"
|
||||
import { randomUUID } from "crypto"
|
||||
import simpleGit, { type SimpleGit } from "simple-git"
|
||||
import { generateBranchName, sanitizeBranchName } from "./branch-name"
|
||||
import type { GitOps } from "./GitOps"
|
||||
import { type GitOps, nonInteractiveEnv } from "./GitOps"
|
||||
import { execWithShellEnv } from "./shell-env"
|
||||
import {
|
||||
parsePRUrl,
|
||||
@@ -662,10 +662,11 @@ export class WorktreeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Either not cached or cache is stale - do the fetch
|
||||
// Either not cached or cache is stale - do the fetch.
|
||||
// Use non-interactive env to prevent SSH passphrase popups.
|
||||
onProgress?.("fetching", `Fetching ${remote}/${branch}...`)
|
||||
try {
|
||||
await this.git.fetch(remote, branch)
|
||||
await simpleGit(this.root).env(nonInteractiveEnv()).fetch(remote, branch)
|
||||
WorktreeManager.fetchCache.set(cacheKey, Date.now())
|
||||
if (await this.refExistsLocally(`${remote}/${branch}`)) {
|
||||
return {
|
||||
|
||||
@@ -188,11 +188,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
else provider.postMessage({ type: "action", action: "historyButtonClicked" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.cycleAgentMode", () => {
|
||||
provider.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
const tab = activeTabProvider()
|
||||
if (tab) tab.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
else provider.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
agentManagerProvider.postMessage({ type: "action", action: "cycleAgentMode" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.cyclePreviousAgentMode", () => {
|
||||
provider.postMessage({ type: "action", action: "cyclePreviousAgentMode" })
|
||||
const tab = activeTabProvider()
|
||||
if (tab) tab.postMessage({ type: "action", action: "cyclePreviousAgentMode" })
|
||||
else provider.postMessage({ type: "action", action: "cyclePreviousAgentMode" })
|
||||
agentManagerProvider.postMessage({ type: "action", action: "cyclePreviousAgentMode" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.profileButtonClicked", () => {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
*/
|
||||
|
||||
import type { KiloClient } from "@kilocode/sdk/v2/client"
|
||||
import type { LegacyMigrationData, MigrationSelections } from "../../legacy-migration/legacy-types"
|
||||
import type {
|
||||
LegacyMigrationData,
|
||||
MigrationSelections,
|
||||
MigrationSessionProgress,
|
||||
} from "../../legacy-migration/legacy-types"
|
||||
import * as MigrationService from "../../legacy-migration/migration-service"
|
||||
|
||||
/** Subset of vscode.ExtensionContext needed by migration handlers. */
|
||||
@@ -31,10 +35,22 @@ export interface MigrationContext {
|
||||
refreshSessions(): void
|
||||
cachedLegacyData: LegacyMigrationData | null
|
||||
migrationCheckInFlight: boolean
|
||||
lastMigrationHadErrors?: boolean
|
||||
disposeGlobal(): Promise<void>
|
||||
broadcastComplete(): void
|
||||
}
|
||||
|
||||
function postSessionProgress(ctx: MigrationContext, progress: MigrationSessionProgress): void {
|
||||
ctx.postMessage({
|
||||
type: "legacyMigrationSessionProgress",
|
||||
session: progress.session,
|
||||
index: progress.index,
|
||||
total: progress.total,
|
||||
phase: progress.phase,
|
||||
error: progress.error,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for legacy data on first run and send migration state to the webview
|
||||
* if the user has not yet been prompted.
|
||||
@@ -111,26 +127,17 @@ export async function handleStartLegacyMigration(
|
||||
(item, status, message) => {
|
||||
ctx.postMessage({ type: "legacyMigrationProgress", item, status, message })
|
||||
},
|
||||
(progress: MigrationSessionProgress) => {
|
||||
postSessionProgress(ctx, progress)
|
||||
},
|
||||
ctx.cachedLegacyData?.settings,
|
||||
ctx.cachedLegacyData?.sessions,
|
||||
)
|
||||
|
||||
const failed = results.some((r) => r.status === "error")
|
||||
const success = results.some((r) => r.status === "success")
|
||||
|
||||
if (!failed && success) {
|
||||
// Dispose all instances after a fully successful migration.
|
||||
// Reloading the data will be handled once the server replies with a global.disposed event.
|
||||
await ctx.disposeGlobal()
|
||||
await MigrationService.setMigrationStatus(
|
||||
ctx.extensionContext as Parameters<typeof MigrationService.setMigrationStatus>[0],
|
||||
"completed",
|
||||
)
|
||||
ctx.broadcastComplete()
|
||||
ctx.refreshSessions()
|
||||
}
|
||||
|
||||
ctx.lastMigrationHadErrors = results.some((item) => item.status === "error")
|
||||
ctx.postMessage({ type: "legacyMigrationComplete", results })
|
||||
} catch (error) {
|
||||
ctx.lastMigrationHadErrors = true
|
||||
console.error("[Kilo New] KiloProvider: ❌ Migration failed", error)
|
||||
ctx.postMessage({
|
||||
type: "legacyMigrationComplete",
|
||||
@@ -146,6 +153,17 @@ export async function handleStartLegacyMigration(
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleFinalizeLegacyMigration(ctx: MigrationContext): Promise<void> {
|
||||
if (!ctx.extensionContext) return
|
||||
await ctx.disposeGlobal()
|
||||
await MigrationService.setMigrationStatus(
|
||||
ctx.extensionContext as Parameters<typeof MigrationService.setMigrationStatus>[0],
|
||||
ctx.lastMigrationHadErrors ? "completed_with_errors" : "completed",
|
||||
)
|
||||
ctx.broadcastComplete()
|
||||
ctx.refreshSessions()
|
||||
}
|
||||
|
||||
/** Record that the user skipped migration and broadcast to all instances. */
|
||||
export async function handleSkipLegacyMigration(ctx: MigrationContext): Promise<void> {
|
||||
if (!ctx.extensionContext) return
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Question handlers — extracted from KiloProvider.
|
||||
*
|
||||
* Manages question reply and reject flows from the tool question UI.
|
||||
* Manages question reply and reject flows from the tool question UI,
|
||||
* plus recovery of pending questions after SSE reconnections or child-session syncs.
|
||||
* No vscode dependency.
|
||||
*/
|
||||
|
||||
@@ -10,10 +11,46 @@ import type { KiloClient } from "@kilocode/sdk/v2/client"
|
||||
interface QuestionContext {
|
||||
readonly client: KiloClient | null
|
||||
readonly currentSessionId: string | undefined
|
||||
readonly trackedSessionIds: Set<string>
|
||||
readonly sessionDirectories: ReadonlyMap<string, string>
|
||||
postMessage(msg: unknown): void
|
||||
getWorkspaceDirectory(sessionId?: string): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all pending questions from the backend and forward any that belong
|
||||
* to tracked sessions to the webview. Mirrors fetchAndSendPendingPermissions —
|
||||
* called after child-session sync and after SSE reconnects so missed
|
||||
* question.asked events don't leave the server blocked indefinitely.
|
||||
*/
|
||||
export async function fetchAndSendPendingQuestions(ctx: QuestionContext): Promise<void> {
|
||||
if (!ctx.client) return
|
||||
try {
|
||||
const dirs = new Set<string>([ctx.getWorkspaceDirectory(), ...ctx.sessionDirectories.values()])
|
||||
const seen = new Set<string>()
|
||||
for (const dir of dirs) {
|
||||
const { data } = await ctx.client.question.list({ directory: dir })
|
||||
if (!data) continue
|
||||
for (const q of data) {
|
||||
if (seen.has(q.id)) continue
|
||||
seen.add(q.id)
|
||||
if (!ctx.trackedSessionIds.has(q.sessionID)) continue
|
||||
ctx.postMessage({
|
||||
type: "questionRequest",
|
||||
question: {
|
||||
id: q.id,
|
||||
sessionID: q.sessionID,
|
||||
questions: q.questions,
|
||||
tool: q.tool,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to fetch pending questions:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle question reply from the webview. */
|
||||
export async function handleQuestionReply(
|
||||
ctx: QuestionContext,
|
||||
|
||||
@@ -299,11 +299,18 @@ export interface MigrationCustomModeInfo {
|
||||
nativeSlug?: string
|
||||
}
|
||||
|
||||
export interface MigrationSessionInfo {
|
||||
id: string
|
||||
title: string
|
||||
directory: string
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface LegacyMigrationData {
|
||||
providers: MigrationProviderInfo[]
|
||||
mcpServers: MigrationMcpServerInfo[]
|
||||
customModes: MigrationCustomModeInfo[]
|
||||
sessions?: string[]
|
||||
sessions?: MigrationSessionInfo[]
|
||||
defaultModel?: { provider: string; model: string }
|
||||
settings?: LegacySettings
|
||||
hasData: boolean
|
||||
@@ -330,11 +337,26 @@ export interface MigrationSettingsSelections {
|
||||
autocomplete: boolean
|
||||
}
|
||||
|
||||
export interface MigrationSessionSelection {
|
||||
id: string
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export interface MigrationSelections {
|
||||
providers: string[]
|
||||
mcpServers: string[]
|
||||
customModes: string[]
|
||||
sessions?: string[]
|
||||
sessions?: MigrationSessionSelection[]
|
||||
defaultModel: boolean
|
||||
settings: MigrationSettingsSelections
|
||||
}
|
||||
|
||||
export type MigrationSessionPhase = "preparing" | "storing" | "skipped" | "done" | "summary" | "error"
|
||||
|
||||
export interface MigrationSessionProgress {
|
||||
session: MigrationSessionInfo
|
||||
index: number
|
||||
total: number
|
||||
phase: MigrationSessionPhase
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ import type {
|
||||
MigrationProviderInfo,
|
||||
MigrationMcpServerInfo,
|
||||
MigrationCustomModeInfo,
|
||||
MigrationSessionInfo,
|
||||
MigrationSessionProgress,
|
||||
} from "./legacy-types"
|
||||
import { buildSessionMeta, buildSessionProgress } from "./migration-session-progress"
|
||||
import type { MigrationResultItem } from "./migration-types"
|
||||
import { createSessionID } from "./sessions/lib/ids"
|
||||
import { migrate as migrateSession } from "./sessions/migrate"
|
||||
@@ -46,7 +49,7 @@ const SECRET_KEY = "roo_cline_config_api_config"
|
||||
const CODEX_OAUTH_SECRET_KEY = "openai-codex-oauth-credentials"
|
||||
const MIGRATION_STATUS_KEY = "kilo.legacyMigrationStatus"
|
||||
|
||||
type MigrationStatus = "completed" | "skipped"
|
||||
type MigrationStatus = "completed" | "completed_with_errors" | "skipped"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status helpers
|
||||
@@ -114,18 +117,27 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis
|
||||
}
|
||||
|
||||
async function readSessionsInGlobalStorage(context: vscode.ExtensionContext) {
|
||||
const items = context.globalState.get<{ id: string }[]>("taskHistory", [])
|
||||
const items = context.globalState.get<{ id: string; task?: string; workspace?: string; ts?: number }[]>(
|
||||
"taskHistory",
|
||||
[],
|
||||
)
|
||||
const base = vscode.Uri.joinPath(context.globalStorageUri, "tasks")
|
||||
const ids: string[] = []
|
||||
const sessions: MigrationSessionInfo[] = []
|
||||
for (const item of items) {
|
||||
const file = vscode.Uri.joinPath(base, item.id, "api_conversation_history.json")
|
||||
const exists = await vscode.workspace.fs.stat(file).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
if (exists) ids.push(item.id)
|
||||
if (!exists) continue
|
||||
sessions.push({
|
||||
id: item.id,
|
||||
title: item.task?.trim() || item.id,
|
||||
directory: item.workspace?.trim() || "",
|
||||
time: item.ts ?? 0,
|
||||
})
|
||||
}
|
||||
return ids
|
||||
return sessions
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -138,6 +150,11 @@ export type ProgressCallback = (
|
||||
message?: string,
|
||||
) => void
|
||||
|
||||
export type SessionProgressCallback = (progress: MigrationSessionProgress) => void
|
||||
|
||||
const SESSION_DELAY = 300
|
||||
const SESSION_SUMMARY_DELAY = 1000
|
||||
|
||||
/**
|
||||
* Executes migration for the selected items.
|
||||
* Calls onProgress for each item with real-time status updates.
|
||||
@@ -150,13 +167,16 @@ export async function migrate(
|
||||
client: KiloClient,
|
||||
selections: MigrationSelections,
|
||||
onProgress: ProgressCallback,
|
||||
onSessionProgress?: SessionProgressCallback,
|
||||
cachedSettings?: LegacySettings,
|
||||
cachedSessions?: MigrationSessionInfo[],
|
||||
): Promise<MigrationResultItem[]> {
|
||||
const profiles = await readLegacyProviderProfiles(context)
|
||||
const mcpSettings = await readLegacyMcpSettings(context)
|
||||
const customModes = await readLegacyCustomModes(context)
|
||||
const prompts = readLegacyCustomModePrompts(context)
|
||||
const legacySettings = cachedSettings ?? readLegacySettings(context)
|
||||
const sessions = cachedSessions ?? (await readSessionsInGlobalStorage(context))
|
||||
|
||||
const results: MigrationResultItem[] = []
|
||||
|
||||
@@ -257,17 +277,35 @@ export async function migrate(
|
||||
}
|
||||
|
||||
if (selections.sessions?.length) {
|
||||
for (const id of selections.sessions) {
|
||||
onProgress(id, "migrating")
|
||||
const result = await migrateSession(id, context, client)
|
||||
const list = selections.sessions
|
||||
for (const [index, item] of list.entries()) {
|
||||
onProgress(item.id, "migrating")
|
||||
const session = sessions.find((entry: MigrationSessionInfo) => entry.id === item.id)
|
||||
const meta = buildSessionMeta(session, index, list.length)
|
||||
const progress = buildSessionProgress(meta, onSessionProgress)
|
||||
const result = await migrateSession(item, context, client, meta, progress)
|
||||
const reason = result.ok ? "Session migrated" : result.message
|
||||
results.push({
|
||||
item: id,
|
||||
item: item.id,
|
||||
category: "session",
|
||||
status: result.ok ? "success" : "error",
|
||||
message: reason,
|
||||
})
|
||||
onProgress(id, result.ok ? "success" : "error", reason)
|
||||
onProgress(item.id, result.ok ? "success" : "error", reason)
|
||||
if (index < list.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, SESSION_DELAY))
|
||||
}
|
||||
}
|
||||
const last = list.at(-1)
|
||||
const session = last ? sessions.find((item: MigrationSessionInfo) => item.id === last.id) : undefined
|
||||
if (session && onSessionProgress) {
|
||||
onSessionProgress({
|
||||
session,
|
||||
index: list.length,
|
||||
total: list.length,
|
||||
phase: "summary",
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, SESSION_SUMMARY_DELAY))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { MigrationSessionInfo, MigrationSessionProgress } from "./legacy-types"
|
||||
import type { SessionProgressCallback } from "./migration-service"
|
||||
|
||||
export interface MigrationSessionMeta {
|
||||
session: MigrationSessionInfo
|
||||
index: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export function buildSessionMeta(
|
||||
session: MigrationSessionInfo | undefined,
|
||||
index: number,
|
||||
total: number,
|
||||
): MigrationSessionMeta | undefined {
|
||||
if (!session) return undefined
|
||||
return {
|
||||
session,
|
||||
index: index + 1,
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSessionProgress(
|
||||
meta: MigrationSessionMeta | undefined,
|
||||
onProgress: SessionProgressCallback | undefined,
|
||||
) {
|
||||
if (!meta || !onProgress) return undefined
|
||||
return (progress: Omit<MigrationSessionProgress, "session" | "index" | "total">) => {
|
||||
onProgress({
|
||||
session: meta.session,
|
||||
index: meta.index,
|
||||
total: meta.total,
|
||||
phase: progress.phase,
|
||||
error: progress.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { KiloClient } from "@kilocode/sdk/v2/client"
|
||||
import { getMigrationErrorMessage } from "../errors/migration-error"
|
||||
import type { MigrationSessionInfo, MigrationSessionProgress, MigrationSessionSelection } from "../legacy-types"
|
||||
import { createSessionID } from "./lib/ids"
|
||||
import type { LegacyHistoryItem } from "./lib/legacy-types"
|
||||
import { parseSession } from "./parser"
|
||||
|
||||
@@ -8,21 +10,71 @@ type Result =
|
||||
| {
|
||||
ok: true
|
||||
skipped?: boolean
|
||||
payload: Awaited<ReturnType<typeof parseSession>>
|
||||
payload?: Awaited<ReturnType<typeof parseSession>>
|
||||
}
|
||||
| {
|
||||
ok: false
|
||||
payload: Awaited<ReturnType<typeof parseSession>>
|
||||
payload?: Awaited<ReturnType<typeof parseSession>>
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function migrate(id: string, context: vscode.ExtensionContext, client: KiloClient): Promise<Result> {
|
||||
type Progress = Omit<MigrationSessionProgress, "session" | "index" | "total">
|
||||
type ProgressCallback = (progress: Progress) => void
|
||||
type Payload = Awaited<ReturnType<typeof parseSession>>
|
||||
|
||||
function trimError(input: string) {
|
||||
return input.length <= 100 ? input : `${input.slice(0, 100)}...`
|
||||
}
|
||||
|
||||
export async function migrate(
|
||||
input: MigrationSessionSelection,
|
||||
context: vscode.ExtensionContext,
|
||||
client: KiloClient,
|
||||
meta?: {
|
||||
session: MigrationSessionInfo
|
||||
index: number
|
||||
total: number
|
||||
},
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<Result> {
|
||||
const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath
|
||||
const items = context.globalState.get<LegacyHistoryItem[]>("taskHistory", [])
|
||||
const item = items.find((item) => item.id === id)
|
||||
const payload = await parseSession(id, dir, item)
|
||||
const item = items.find((item) => item.id === input.id)
|
||||
|
||||
const progress = (next: Progress) => {
|
||||
if (!meta || !onProgress) return
|
||||
onProgress(next)
|
||||
}
|
||||
|
||||
const skip = () => {
|
||||
progress({ phase: "skipped" })
|
||||
return {
|
||||
ok: true as const,
|
||||
skipped: true,
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (error: unknown, payload?: Payload) => {
|
||||
progress({
|
||||
phase: "error",
|
||||
error: trimError(getMigrationErrorMessage(error)),
|
||||
})
|
||||
return {
|
||||
ok: false as const,
|
||||
...(payload ? { payload } : {}),
|
||||
message: getMigrationErrorMessage(error),
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!input.force) {
|
||||
const result = await client.session.get({ sessionID: createSessionID(input.id) })
|
||||
if (result.data) return skip()
|
||||
}
|
||||
|
||||
progress({ phase: "preparing" })
|
||||
const payload = await parseSession(input.id, dir, item)
|
||||
progress({ phase: "storing" })
|
||||
const project = await client.kilocode.sessionImport.project(payload.project, { throwOnError: true })
|
||||
const projectID = project.data?.id ?? payload.project.id
|
||||
const session = await client.kilocode.sessionImport.session(
|
||||
@@ -31,11 +83,13 @@ export async function migrate(id: string, context: vscode.ExtensionContext, clie
|
||||
projectID,
|
||||
query_directory: payload.session.directory,
|
||||
body_directory: payload.session.directory,
|
||||
...(input.force ? { force: true } : {}),
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
// Skip child imports when the session already exists so rerunning migration only imports missing sessions.
|
||||
if (session.data?.skipped) {
|
||||
progress({ phase: "skipped" })
|
||||
return {
|
||||
ok: true,
|
||||
skipped: true,
|
||||
@@ -51,15 +105,13 @@ export async function migrate(id: string, context: vscode.ExtensionContext, clie
|
||||
await client.kilocode.sessionImport.part(part, { throwOnError: true })
|
||||
}
|
||||
|
||||
progress({ phase: "done" })
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
payload,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
payload,
|
||||
message: getMigrationErrorMessage(error),
|
||||
}
|
||||
return fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export class ServerManager {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("[Kilo New] ServerManager: 🎬 Spawning CLI process:", cliPath, ["serve", "--port", "0"])
|
||||
const claudeCompat = vscode.workspace.getConfiguration("kilo-code.new").get<boolean>("claudeCodeCompat", false)
|
||||
const serverProcess = spawn(cliPath, ["serve", "--port", "0"], {
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -80,6 +81,7 @@ export class ServerManager {
|
||||
KILO_MACHINE_ID: vscode.env.machineId,
|
||||
KILO_APP_VERSION: this.context.extension.packageJSON.version,
|
||||
KILO_VSCODE_VERSION: vscode.version,
|
||||
...(!claudeCompat && { KILO_DISABLE_CLAUDE_CODE: "true" }),
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
|
||||
@@ -4,12 +4,8 @@ import * as os from "os"
|
||||
import * as nodePath from "path"
|
||||
import { GitOps } from "../../src/agent-manager/GitOps"
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function ops(handler: (args: string[], cwd: string) => Promise<string>): GitOps {
|
||||
return new GitOps({ log: () => undefined, refreshMs: 120000, runGit: handler })
|
||||
return new GitOps({ log: () => undefined, runGit: handler })
|
||||
}
|
||||
|
||||
function runGit(cwd: string, args: string[]): string {
|
||||
@@ -193,106 +189,29 @@ describe("GitOps", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("refreshRemote", () => {
|
||||
it("fetches the remote", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
|
||||
return ""
|
||||
})
|
||||
await git.refreshRemote("/repo", "origin")
|
||||
const fetches = commands.filter((c) => c[0] === "fetch")
|
||||
expect(fetches.length).toBe(1)
|
||||
expect(fetches[0]![3]).toBe("origin")
|
||||
})
|
||||
|
||||
it("skips empty remote name", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
commands.push(args)
|
||||
return ""
|
||||
})
|
||||
await git.refreshRemote("/repo", "")
|
||||
expect(commands.length).toBe(0)
|
||||
})
|
||||
|
||||
it("throttles repeated fetches for the same remote", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
|
||||
return ""
|
||||
})
|
||||
await git.refreshRemote("/repo", "origin")
|
||||
await git.refreshRemote("/repo", "origin")
|
||||
const fetches = commands.filter((c) => c[0] === "fetch")
|
||||
expect(fetches.length).toBe(1)
|
||||
})
|
||||
|
||||
it("deduplicates inflight fetches", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = new GitOps({
|
||||
log: () => undefined,
|
||||
refreshMs: 0,
|
||||
runGit: async (args) => {
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
|
||||
if (args[0] === "fetch") {
|
||||
await sleep(50)
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
},
|
||||
})
|
||||
await Promise.all([git.refreshRemote("/repo", "origin"), git.refreshRemote("/repo", "origin")])
|
||||
const fetches = commands.filter((c) => c[0] === "fetch")
|
||||
expect(fetches.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("aheadBehind", () => {
|
||||
it("counts commits ahead and behind using the provided ref", async () => {
|
||||
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") return "1\t3"
|
||||
return ""
|
||||
})
|
||||
expect(await git.aheadBehind("/repo", "origin/main")).toEqual({ ahead: 3, behind: 1 })
|
||||
})
|
||||
|
||||
it("fetches the explicitly-provided remote before counting", async () => {
|
||||
it("does not fetch from remote", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
commands.push(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") return "0\t4"
|
||||
return ""
|
||||
})
|
||||
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("skips fetch when no remote is provided", async () => {
|
||||
const commands: string[][] = []
|
||||
const git = ops(async (args) => {
|
||||
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 })
|
||||
await git.aheadBehind("/repo", "myfork/main")
|
||||
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] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list") throw new Error("fatal")
|
||||
return ""
|
||||
})
|
||||
@@ -302,8 +221,6 @@ describe("GitOps", () => {
|
||||
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"
|
||||
|
||||
@@ -66,9 +66,6 @@ describe("GitStatsPoller", () => {
|
||||
log: () => undefined,
|
||||
intervalMs: 5,
|
||||
git: gitOps(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t1"
|
||||
return ""
|
||||
}),
|
||||
@@ -106,9 +103,7 @@ describe("GitStatsPoller", () => {
|
||||
log: () => undefined,
|
||||
intervalMs: 5,
|
||||
git: gitOps(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t2"
|
||||
return ""
|
||||
}),
|
||||
@@ -231,9 +226,7 @@ describe("GitStatsPoller", () => {
|
||||
if (args[0] === "worktree") {
|
||||
return `worktree ${wtAPath}\nbranch refs/heads/branch-a\n`
|
||||
}
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
|
||||
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list") return "1"
|
||||
return ""
|
||||
}),
|
||||
@@ -287,8 +280,6 @@ describe("GitStatsPoller", () => {
|
||||
git: gitOps(async (args) => {
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "HEAD") return "feature"
|
||||
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/feature"
|
||||
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 "0\t3"
|
||||
if (args[0] === "branch") return "feature"
|
||||
if (args[0] === "config") return "origin"
|
||||
@@ -341,8 +332,6 @@ describe("GitStatsPoller", () => {
|
||||
// myfork/HEAD resolves to the default branch
|
||||
if (args[0] === "symbolic-ref" && args[2] === "refs/remotes/myfork/HEAD") return "myfork/develop"
|
||||
if (args[0] === "branch") return "my-feature"
|
||||
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 "0\t5"
|
||||
return ""
|
||||
}),
|
||||
@@ -406,7 +395,7 @@ describe("GitStatsPoller", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("refreshes upstream remote once for concurrent worktrees", async () => {
|
||||
it("does not fetch from remote for ahead/behind counts", async () => {
|
||||
const commands: string[][] = []
|
||||
const emitted: Array<
|
||||
Array<{ worktreeId: string; files: number; additions: number; deletions: number; ahead: number; behind: number }>
|
||||
@@ -416,7 +405,6 @@ describe("GitStatsPoller", () => {
|
||||
worktree: { diffSummary: 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", "upstream"), worktree("b", "upstream")],
|
||||
getWorkspaceRoot: () => undefined,
|
||||
@@ -427,8 +415,6 @@ describe("GitStatsPoller", () => {
|
||||
intervalMs: 500,
|
||||
git: gitOps(async (args) => {
|
||||
commands.push(args)
|
||||
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
|
||||
if (args[0] === "fetch") return ""
|
||||
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0"
|
||||
return ""
|
||||
}),
|
||||
@@ -439,9 +425,6 @@ describe("GitStatsPoller", () => {
|
||||
poller.stop()
|
||||
|
||||
const fetches = commands.filter((cmd) => cmd[0] === "fetch")
|
||||
expect(fetches.length).toBe(1)
|
||||
const fetch = fetches[0]
|
||||
if (!fetch) throw new Error("expected fetch command")
|
||||
expect(fetch[3]).toBe("upstream")
|
||||
expect(fetches.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -363,11 +363,21 @@ describe("mapSSEEventToWebviewMessage", () => {
|
||||
properties: {
|
||||
id: "q1",
|
||||
sessionID: "sess-1",
|
||||
questions: [],
|
||||
questions: [
|
||||
{
|
||||
question: "Ready to implement?",
|
||||
header: "Implement",
|
||||
options: [{ label: "Implement", description: "Switch to code", mode: "code" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
|
||||
expect(msg?.type).toBe("questionRequest")
|
||||
if (msg?.type === "questionRequest") {
|
||||
const questions = msg.question.questions as Array<{ options?: Array<{ mode?: string }> }>
|
||||
expect(questions[0]?.options?.[0]?.mode).toBe("code")
|
||||
}
|
||||
})
|
||||
|
||||
it("maps question.replied to questionResolved", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
import { createSessionID } from "../../../src/legacy-migration/sessions/lib/ids"
|
||||
|
||||
const parseSession = mock(async () => ({
|
||||
project: {
|
||||
@@ -43,8 +44,12 @@ function ctx() {
|
||||
|
||||
function client() {
|
||||
const calls: Array<{ name: string; body: unknown }> = []
|
||||
const get = mock(async () => ({ data: null as { id: string } | null }))
|
||||
return {
|
||||
calls,
|
||||
session: {
|
||||
get,
|
||||
},
|
||||
kilocode: {
|
||||
sessionImport: {
|
||||
project: async (body: unknown) => {
|
||||
@@ -73,9 +78,57 @@ describe("legacy migration migrate", () => {
|
||||
parseSession.mockClear()
|
||||
})
|
||||
|
||||
it("skips before parsing when the migrated session already exists and force is false", async () => {
|
||||
const api = {
|
||||
session: {
|
||||
get: async (body: unknown) => {
|
||||
expect(body).toEqual({ sessionID: createSessionID("legacy-task-1") })
|
||||
return { data: { id: createSessionID("legacy-task-1") } }
|
||||
},
|
||||
},
|
||||
kilocode: {
|
||||
sessionImport: {
|
||||
project: async () => {
|
||||
throw new Error("should not import project")
|
||||
},
|
||||
session: async () => {
|
||||
throw new Error("should not import session")
|
||||
},
|
||||
message: async () => {
|
||||
throw new Error("should not import message")
|
||||
},
|
||||
part: async () => {
|
||||
throw new Error("should not import part")
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = await migrate({ id: "legacy-task-1" }, ctx() as never, api as never)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result).toHaveProperty("skipped", true)
|
||||
expect(parseSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("continues with import when force is true even if the migrated session already exists", async () => {
|
||||
const api = client()
|
||||
api.session.get.mockImplementation(async () => ({ data: { id: createSessionID("legacy-task-1") } }))
|
||||
|
||||
await migrate({ id: "legacy-task-1", force: true }, ctx() as never, api as never)
|
||||
|
||||
expect(parseSession).toHaveBeenCalledTimes(1)
|
||||
const call = api.calls.find((item) => item.name === "session")
|
||||
expect(call).toBeDefined()
|
||||
expect((call?.body as { force?: boolean }).force).toBe(true)
|
||||
})
|
||||
|
||||
it("inserts project then session then message then part in the correct order", async () => {
|
||||
const calls: string[] = []
|
||||
const api = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
},
|
||||
kilocode: {
|
||||
sessionImport: {
|
||||
project: async () => {
|
||||
@@ -144,7 +197,7 @@ describe("legacy migration migrate", () => {
|
||||
] as never,
|
||||
})
|
||||
|
||||
await migrate("legacy-task-1", ctx() as never, api as never)
|
||||
await migrate({ id: "legacy-task-1" }, ctx() as never, api as never)
|
||||
|
||||
expect(calls).toEqual(["project", "session", "message", "part"])
|
||||
})
|
||||
@@ -152,7 +205,7 @@ describe("legacy migration migrate", () => {
|
||||
it("uses the projectID returned by backend when inserting the session", async () => {
|
||||
const api = client()
|
||||
|
||||
await migrate("legacy-task-1", ctx() as never, api as never)
|
||||
await migrate({ id: "legacy-task-1" }, ctx() as never, api as never)
|
||||
|
||||
const call = api.calls.find((x) => x.name === "session")
|
||||
expect(call).toBeDefined()
|
||||
@@ -161,6 +214,9 @@ describe("legacy migration migrate", () => {
|
||||
|
||||
it("returns ok false without throwing when one backend insert fails", async () => {
|
||||
const api = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
},
|
||||
kilocode: {
|
||||
sessionImport: {
|
||||
project: async () => ({ data: { id: "project-real" } }),
|
||||
@@ -173,7 +229,7 @@ describe("legacy migration migrate", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const result = await migrate("legacy-task-1", ctx() as never, api as never)
|
||||
const result = await migrate({ id: "legacy-task-1" }, ctx() as never, api as never)
|
||||
|
||||
expect(result.ok).toBe(false)
|
||||
})
|
||||
@@ -181,6 +237,9 @@ describe("legacy migration migrate", () => {
|
||||
it("skips message and part imports when backend reports the session already exists", async () => {
|
||||
const calls: string[] = []
|
||||
const api = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
},
|
||||
kilocode: {
|
||||
sessionImport: {
|
||||
project: async () => {
|
||||
@@ -203,7 +262,7 @@ describe("legacy migration migrate", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const result = await migrate("legacy-task-1", ctx() as never, api as never)
|
||||
const result = await migrate({ id: "legacy-task-1" }, ctx() as never, api as never)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result).toHaveProperty("skipped", true)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
createSessionItem,
|
||||
createSessionSummary,
|
||||
updateSessionSummary,
|
||||
} from "../../../webview-ui/src/components/migration/session-migration-summary-state"
|
||||
|
||||
const session = createSessionItem({
|
||||
id: "legacy-1",
|
||||
title: "Legacy task",
|
||||
directory: "/workspace/testing",
|
||||
time: 1,
|
||||
})
|
||||
|
||||
describe("session migration summary state", () => {
|
||||
it("moves a session from skipped to imported when a force rerun finishes successfully", () => {
|
||||
const skipped = updateSessionSummary(createSessionSummary(), session, "skipped")
|
||||
const done = updateSessionSummary(skipped, session, "done")
|
||||
|
||||
expect(done.skipped).toHaveLength(0)
|
||||
expect(done.errored).toHaveLength(0)
|
||||
expect(done.imported).toEqual([session])
|
||||
})
|
||||
|
||||
it("moves a session into errored and removes it from the other buckets", () => {
|
||||
const done = updateSessionSummary(createSessionSummary(), session, "done")
|
||||
const next = updateSessionSummary(done, { ...session, error: "Boom\nstack" }, "error")
|
||||
|
||||
expect(next.imported).toHaveLength(0)
|
||||
expect(next.skipped).toHaveLength(0)
|
||||
expect(next.errored).toEqual([{ ...session, error: "Boom\nstack" }])
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { toggleAnswer } from "../../webview-ui/src/components/chat/question-dock-utils"
|
||||
import {
|
||||
resolveOptimisticQuestionAgent,
|
||||
resolveQuestionMode,
|
||||
resolveSelectedQuestionMode,
|
||||
toggleAnswer,
|
||||
} from "../../webview-ui/src/components/chat/question-dock-utils"
|
||||
|
||||
describe("toggleAnswer", () => {
|
||||
it("adds answer when not present", () => {
|
||||
@@ -36,3 +41,90 @@ describe("toggleAnswer", () => {
|
||||
expect(result).toEqual(["a"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveQuestionMode", () => {
|
||||
it("returns mode for matching predefined option", () => {
|
||||
const result = resolveQuestionMode(
|
||||
[
|
||||
{ label: "Implement", description: "Switch to code", mode: "code" },
|
||||
{ label: "Stay", description: "Remain here" },
|
||||
],
|
||||
"Implement",
|
||||
)
|
||||
|
||||
expect(result).toBe("code")
|
||||
})
|
||||
|
||||
it("returns undefined for unknown answer", () => {
|
||||
const result = resolveQuestionMode([{ label: "Implement", description: "Switch", mode: "code" }], "Custom")
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns undefined when option has no mode", () => {
|
||||
const result = resolveQuestionMode([{ label: "Stay", description: "Remain here" }], "Stay")
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveSelectedQuestionMode", () => {
|
||||
it("returns the selected mode from predefined answers", () => {
|
||||
const result = resolveSelectedQuestionMode(
|
||||
[
|
||||
[
|
||||
{ label: "Implement", description: "Switch to code", mode: "code" },
|
||||
{ label: "Stay", description: "Remain here" },
|
||||
],
|
||||
].map((options) => ({ options })),
|
||||
[["Implement"]],
|
||||
)
|
||||
|
||||
expect(result).toBe("code")
|
||||
})
|
||||
|
||||
it("ignores custom answers that replace a mode option", () => {
|
||||
const result = resolveSelectedQuestionMode(
|
||||
[{ options: [{ label: "Implement", description: "Switch to code", mode: "code" }] }],
|
||||
[["Implement custom flow"]],
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("keeps mode answers from other questions", () => {
|
||||
const result = resolveSelectedQuestionMode(
|
||||
[
|
||||
{ options: [{ label: "Implement", description: "Switch to code", mode: "code" }] },
|
||||
{ options: [{ label: "Stay", description: "Remain here" }] },
|
||||
],
|
||||
[["Implement"], ["Stay"]],
|
||||
)
|
||||
|
||||
expect(result).toBe("code")
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveOptimisticQuestionAgent", () => {
|
||||
it("stores the previous agent when applying an optimistic mode", () => {
|
||||
const result = resolveOptimisticQuestionAgent(undefined, "ask", "code")
|
||||
|
||||
expect(result).toEqual({ base: "ask", agent: "code" })
|
||||
})
|
||||
|
||||
it("reverts to the stored previous agent when the mode is cleared", () => {
|
||||
const result = resolveOptimisticQuestionAgent("ask", "code", undefined)
|
||||
|
||||
expect(result).toEqual({ base: undefined, agent: "ask" })
|
||||
})
|
||||
|
||||
it("avoids switching when the selected mode already matches the current agent", () => {
|
||||
const result = resolveOptimisticQuestionAgent(undefined, "code", "code")
|
||||
|
||||
expect(result).toEqual({ base: undefined, agent: undefined })
|
||||
})
|
||||
|
||||
it("keeps the original base agent while changing between mode answers", () => {
|
||||
const result = resolveOptimisticQuestionAgent("ask", "code", "architect")
|
||||
|
||||
expect(result).toEqual({ base: "ask", agent: "architect" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { resolveSessionAgent } from "../../webview-ui/src/context/session-agent"
|
||||
import type { Message } from "../../webview-ui/src/types/messages"
|
||||
|
||||
function makeMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
id: "msg-1",
|
||||
sessionID: "sess-1",
|
||||
role: "user",
|
||||
createdAt: new Date(0).toISOString(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe("resolveSessionAgent", () => {
|
||||
it("returns the latest valid user agent", () => {
|
||||
const result = resolveSessionAgent(
|
||||
[
|
||||
makeMessage({ id: "1", agent: "plan" }),
|
||||
makeMessage({ id: "2", role: "assistant", agent: "ask" }),
|
||||
makeMessage({ id: "3", agent: "code" }),
|
||||
],
|
||||
new Set(["plan", "code", "ask"]),
|
||||
)
|
||||
|
||||
expect(result).toBe("code")
|
||||
})
|
||||
|
||||
it("ignores assistant messages", () => {
|
||||
const result = resolveSessionAgent(
|
||||
[makeMessage({ role: "assistant", agent: "code" }), makeMessage({ agent: "plan" })],
|
||||
new Set(["plan", "code"]),
|
||||
)
|
||||
|
||||
expect(result).toBe("plan")
|
||||
})
|
||||
|
||||
it("ignores unknown agent names", () => {
|
||||
const result = resolveSessionAgent(
|
||||
[makeMessage({ agent: "missing" }), makeMessage({ agent: "code" })],
|
||||
new Set(["code"]),
|
||||
)
|
||||
|
||||
expect(result).toBe("code")
|
||||
})
|
||||
|
||||
it("ignores empty agent values", () => {
|
||||
const result = resolveSessionAgent([makeMessage({ agent: " " })], new Set(["code"]))
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns undefined when no valid user agent exists", () => {
|
||||
const result = resolveSessionAgent(
|
||||
[makeMessage({ role: "assistant", agent: "code" }), makeMessage({ agent: undefined })],
|
||||
new Set(["code"]),
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { color, palette, label } from "../../webview-ui/src/utils/timeline/colors"
|
||||
import type {
|
||||
Part,
|
||||
ToolPart,
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
} from "../../webview-ui/src/types/messages"
|
||||
|
||||
function mkText(text = "hello"): TextPart {
|
||||
return { id: "t1", type: "text", text }
|
||||
}
|
||||
|
||||
function mkReasoning(text = "thinking..."): ReasoningPart {
|
||||
return { id: "r1", type: "reasoning", text }
|
||||
}
|
||||
|
||||
function mkTool(name: string, status: "pending" | "running" | "completed" | "error" = "completed"): ToolPart {
|
||||
const base = { id: "tool1", type: "tool" as const, tool: name }
|
||||
if (status === "pending") return { ...base, state: { status: "pending", input: {} } }
|
||||
if (status === "running") return { ...base, state: { status: "running", input: {} } }
|
||||
if (status === "error") return { ...base, state: { status: "error", input: {}, error: "fail" } }
|
||||
return { ...base, state: { status: "completed", input: {}, output: "ok", title: name } }
|
||||
}
|
||||
|
||||
function mkStepStart(): StepStartPart {
|
||||
return { id: "ss1", type: "step-start" }
|
||||
}
|
||||
|
||||
function mkStepFinish(): StepFinishPart {
|
||||
return { id: "sf1", type: "step-finish", reason: "done" }
|
||||
}
|
||||
|
||||
describe("timeline colors", () => {
|
||||
it("classifies text parts as text color", () => {
|
||||
expect(color(mkText())).toBe(palette.text)
|
||||
})
|
||||
|
||||
it("classifies reasoning parts as reasoning color", () => {
|
||||
expect(color(mkReasoning())).toBe(palette.reasoning)
|
||||
})
|
||||
|
||||
it("classifies read tools as read color", () => {
|
||||
expect(color(mkTool("read"))).toBe(palette.read)
|
||||
expect(color(mkTool("glob"))).toBe(palette.read)
|
||||
expect(color(mkTool("grep"))).toBe(palette.read)
|
||||
expect(color(mkTool("ls"))).toBe(palette.read)
|
||||
expect(color(mkTool("diagnostics"))).toBe(palette.read)
|
||||
expect(color(mkTool("warpgrep"))).toBe(palette.read)
|
||||
})
|
||||
|
||||
it("classifies write tools as write color", () => {
|
||||
expect(color(mkTool("edit"))).toBe(palette.write)
|
||||
expect(color(mkTool("write"))).toBe(palette.write)
|
||||
expect(color(mkTool("patch"))).toBe(palette.write)
|
||||
expect(color(mkTool("multiedit"))).toBe(palette.write)
|
||||
expect(color(mkTool("apply_patch"))).toBe(palette.write)
|
||||
})
|
||||
|
||||
it("classifies generic tools as tool color", () => {
|
||||
expect(color(mkTool("bash"))).toBe(palette.tool)
|
||||
expect(color(mkTool("task"))).toBe(palette.tool)
|
||||
expect(color(mkTool("browser"))).toBe(palette.tool)
|
||||
})
|
||||
|
||||
it("classifies errored tools as error color", () => {
|
||||
expect(color(mkTool("bash", "error"))).toBe(palette.error)
|
||||
expect(color(mkTool("read", "error"))).toBe(palette.error)
|
||||
})
|
||||
|
||||
it("classifies step-start as step color", () => {
|
||||
expect(color(mkStepStart())).toBe(palette.step)
|
||||
})
|
||||
|
||||
it("classifies step-finish as success color", () => {
|
||||
expect(color(mkStepFinish())).toBe(palette.success)
|
||||
})
|
||||
|
||||
it("returns fallback for unknown part types", () => {
|
||||
const weird = { id: "w1", type: "snapshot" } as unknown as Part
|
||||
expect(color(weird)).toBe(palette.fallback)
|
||||
})
|
||||
})
|
||||
|
||||
describe("timeline labels", () => {
|
||||
it("returns 'Text' for text parts", () => {
|
||||
expect(label(mkText())).toBe("Text")
|
||||
})
|
||||
|
||||
it("returns 'Reasoning' for reasoning parts", () => {
|
||||
expect(label(mkReasoning())).toBe("Reasoning")
|
||||
})
|
||||
|
||||
it("returns tool name for tool parts", () => {
|
||||
expect(label(mkTool("bash"))).toBe("bash")
|
||||
expect(label(mkTool("read"))).toBe("read")
|
||||
})
|
||||
|
||||
it("returns 'Step start' for step-start", () => {
|
||||
expect(label(mkStepStart())).toBe("Step start")
|
||||
})
|
||||
|
||||
it("returns 'Step finish' for step-finish", () => {
|
||||
expect(label(mkStepFinish())).toBe("Step finish")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { sizes, MAX_HEIGHT } from "../../webview-ui/src/utils/timeline/sizes"
|
||||
import type { Part, TextPart, ToolPart, StepFinishPart } from "../../webview-ui/src/types/messages"
|
||||
|
||||
function mkText(text: string): TextPart {
|
||||
return { id: `t-${text.length}`, type: "text", text }
|
||||
}
|
||||
|
||||
function mkTool(name: string, input: Record<string, unknown> = {}, output = ""): ToolPart {
|
||||
return {
|
||||
id: `tool-${name}`,
|
||||
type: "tool",
|
||||
tool: name,
|
||||
state: { status: "completed", input, output, title: name },
|
||||
}
|
||||
}
|
||||
|
||||
function mkStepFinish(input = 100, output = 50): StepFinishPart {
|
||||
return {
|
||||
id: "sf",
|
||||
type: "step-finish",
|
||||
reason: "done",
|
||||
tokens: { input, output },
|
||||
}
|
||||
}
|
||||
|
||||
describe("timeline sizes", () => {
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(sizes([])).toEqual([])
|
||||
})
|
||||
|
||||
it("returns one entry per part", () => {
|
||||
const parts: Part[] = [mkText("a"), mkText("bb"), mkText("ccc")]
|
||||
const result = sizes(parts)
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("all bars have uniform width", () => {
|
||||
const parts: Part[] = [mkText("short"), mkText("a".repeat(500)), mkText("medium")]
|
||||
const result = sizes(parts)
|
||||
const w = result[0]!.width
|
||||
for (const bar of result) {
|
||||
expect(bar.width).toBe(w)
|
||||
}
|
||||
})
|
||||
|
||||
it("height stays within bounds", () => {
|
||||
const parts: Part[] = [mkText("short"), mkText("a".repeat(500)), mkText("medium length text")]
|
||||
const result = sizes(parts)
|
||||
for (const bar of result) {
|
||||
expect(bar.height).toBeGreaterThanOrEqual(8)
|
||||
expect(bar.height).toBeLessThanOrEqual(MAX_HEIGHT)
|
||||
}
|
||||
})
|
||||
|
||||
it("larger content produces taller bars", () => {
|
||||
const parts: Part[] = [mkText("x"), mkText("x".repeat(1000))]
|
||||
const result = sizes(parts)
|
||||
expect(result[1]!.height).toBeGreaterThan(result[0]!.height)
|
||||
})
|
||||
|
||||
it("handles tool parts with input/output content", () => {
|
||||
const parts: Part[] = [
|
||||
mkTool("bash", { command: "ls" }, "file1\nfile2\nfile3"),
|
||||
mkTool("read", { path: "README.md" }, "a".repeat(200)),
|
||||
]
|
||||
const result = sizes(parts)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]!.content).toBeGreaterThan(0)
|
||||
expect(result[1]!.content).toBeGreaterThan(result[0]!.content)
|
||||
})
|
||||
|
||||
it("handles step-finish parts using token counts", () => {
|
||||
const parts: Part[] = [mkStepFinish(1000, 500), mkStepFinish(100, 50)]
|
||||
const result = sizes(parts)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]!.content).toBeGreaterThan(result[1]!.content)
|
||||
})
|
||||
|
||||
it("handles single-part input without crashing", () => {
|
||||
const result = sizes([mkText("only one")])
|
||||
expect(result).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("returns integer values for height", () => {
|
||||
const parts: Part[] = [mkText("a"), mkText("bb"), mkText("ccc"), mkText("dddd")]
|
||||
const result = sizes(parts)
|
||||
for (const bar of result) {
|
||||
expect(Number.isInteger(bar.height)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6442d82e137e36d165e5f10ca7dbf02445badf109d442eac05714c50593068fe
|
||||
size 7819
|
||||
oid sha256:e929c1f6dd90495d14edea552acfd2f1a3a609d1c3713651f5e11f40aac19309
|
||||
size 8120
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00c00caa93376d82f4128001416fe14ee71f786f0bff3d6550dc97ff9a675037
|
||||
size 27167
|
||||
oid sha256:024ac7b12c5c6455d30c349dbfde0faff8d8c864761d0d79f0f9e08dbf7144e0
|
||||
size 26331
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7576fe3532b45c91e0d4d65c97b515565e7b3ad26d6d713d0d52fdab0e0c0565
|
||||
size 29627
|
||||
oid sha256:2ff364be7ea176328dc7f4500e3568aed460a2caa9dd4dedca614cd1db273185
|
||||
size 25150
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a69ac58dc72885950a702661b75bc1989a28e8b07b3136037255891b7e5a3b06
|
||||
size 12839
|
||||
oid sha256:070f9f8cb76fc01ceba027cbcaf198e0530545a0aa78ea1ead75f1e09bb252a1
|
||||
size 14152
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30346f68598419d1fe80a3f33d3326cc844a30356ebedb022a841be4096514c6
|
||||
size 53909
|
||||
oid sha256:5094e214a52bac4627f8fe280f4818c986753c49f160bf5295f21122054ea72f
|
||||
size 53684
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e654374e5c12a4a184a0d27709cd773684e31e08a3d7b6305358849cf30c75be
|
||||
size 50986
|
||||
oid sha256:83a70ef1260f77c3017db90a918ee77c332c065bfc047e0f206948bb5dde83b9
|
||||
size 50888
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7e17426d82ff00ac8194963a3c3be946994018e2f6c9553dfaee237cb043fd9
|
||||
size 11324
|
||||
oid sha256:b569b91c3437b55163cc48a2fcf6f5b0ebd2c6a93aff425b14d70f6055201f20
|
||||
size 12716
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e2eb5ca750d8a590df5a4c7a1be493290aa90535a98c5dc0f6fadfc802872567
|
||||
size 3520
|
||||
oid sha256:e2bb93bd5830bcfd208615e64c8b2b88855d1faa97cd7980df9d95cfada533d3
|
||||
size 3561
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e5fd252dcc1acaeefae5608621a1c32282f99c24f7ee464d375cd149722c4fc
|
||||
size 3915
|
||||
oid sha256:862a22d4123e5c512d4ae87eb881c5923f14ac5207bf77b4707063b3dcc53422
|
||||
size 3991
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2da4bbf78414fd58ef84b03f421954741edae16df0a0fccc4ad4e99e752e70ff
|
||||
size 3882
|
||||
oid sha256:c49479d1fffe96d14300617d48be269e5bffe41bc50f555b1ee3fbb09d8919e1
|
||||
size 3896
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user