diff --git a/.gitattributes b/.gitattributes
index d351e0fc83..3044164203 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -18,3 +18,7 @@ packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
**/i18n/parity.test.ts linguist-generated=false
packages/kilo-i18n/src/*.ts linguist-generated=true
packages/kilo-i18n/src/en.ts linguist-generated=false
+
+# Auto-generated CLI reference docs
+packages/kilo-docs/markdoc/partials/cli-commands-table.md linguist-generated=true
+packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md linguist-generated=true
diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml
new file mode 100644
index 0000000000..b2fc705dae
--- /dev/null
+++ b/.github/workflows/check-opencode-annotations.yml
@@ -0,0 +1,32 @@
+name: Check opencode annotations
+
+on:
+ pull_request:
+ paths:
+ - "packages/opencode/**"
+ - "script/check-opencode-annotations.ts"
+ - ".github/workflows/check-opencode-annotations.yml"
+ workflow_dispatch:
+
+jobs:
+ check-annotations:
+ name: Check kilocode_change annotations
+ if: github.repository == 'Kilo-Org/kilocode'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 0
+
+ - uses: oven-sh/setup-bun@v2
+
+ - name: Check kilocode_change annotations in shared opencode files
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ run: |
+ if [ -n "$BASE_SHA" ]; then
+ bun run script/check-opencode-annotations.ts --base "$BASE_SHA"
+ else
+ echo "No PR base SHA available (workflow_dispatch without PR context) — skipping."
+ fi
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8b64bf4ed5..9fc57f520d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,7 +23,7 @@ on:
required: false
type: string
pre_release:
- description: "Publish as pre-release (VS Code marketplace)"
+ description: "Publish as pre-release (VS Code marketplace + npm rc channel)"
required: false
type: boolean
default: false
@@ -58,6 +58,7 @@ jobs:
GH_REPO: ${{ github.repository }}
KILO_BUMP: ${{ inputs.bump }}
KILO_VERSION: ${{ inputs.version }}
+ KILO_PRE_RELEASE: ${{ inputs.pre_release }}
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
outputs:
@@ -83,6 +84,7 @@ jobs:
env:
KILO_VERSION: ${{ needs.version.outputs.version }}
KILO_RELEASE: ${{ needs.version.outputs.release }}
+ KILO_PRE_RELEASE: ${{ inputs.pre_release }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
diff --git a/AGENTS.md b/AGENTS.md
index a832386082..8f3ed22f50 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,6 +10,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
## Build and Dev
- **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts`
+- **Dev with params**: `bun dev -- help`
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build.
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`)
- **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests)
@@ -18,6 +19,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
- **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing.
- **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale.
- **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing.
+- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
## Products
@@ -173,6 +175,8 @@ Tests MUST test actual implementation, do not duplicate logic into a test.
Kilo CLI is a fork of [opencode](https://github.com/anomalyco/opencode).
+**Very important**: when planning or coding, update shared files with OpenCode as last resort! Everything is shared code from OpenCode, except folders that contain `kilo` in the name or have a parent directory that contains `kilo` in the name. Example of kilo specific folders: `packages/opencode/src/kilocode/` and `packages/kilo-docs/`. Always look for ways to implement your feature or fix in a way that minimizes changes to shared code.
+
### Minimizing Merge Conflicts
We regularly merge upstream changes from opencode. To minimize merge conflicts and keep the sync process smooth:
@@ -216,6 +220,21 @@ const bar = 2
// kilocode_change - new file
```
+
+**JSX/TSX (inside JSX templates):**
+
+
+```tsx
+{/* kilocode_change */}
+```
+
+
+```tsx
+{/* kilocode_change start */}
+
+{/* kilocode_change end */}
+```
+
#### When markers are NOT needed
Code in these paths is Kilo Code-specific and does NOT need `kilocode_change` markers:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 65565b37ab..d7950ddfe8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -72,12 +72,10 @@ During development, `bun dev` is the local equivalent of the built `kilo` comman
# Development (from project root)
bun dev --help # Show all available commands
bun dev serve # Start headless API server
-bun dev web # Start server + open web interface
# Production
kilo --help # Show all available commands
kilo serve # Start headless API server
-kilo web # Start server + open web interface
```
### Testing with a local backend
diff --git a/README.md b/README.md
index e77e3fbbf7..2199ffd41a 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
- ⚡ Inline autocomplete suggestions
- 🤖 Latest AI models
- 🎁 API keys optional
-- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.2
+- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4
## Quick Links
@@ -38,7 +38,7 @@
## Get Started in Visual Studio Code
1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code).
-2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 – with transparent pricing that matches provider rates exactly.
+2. Create your account to access 500+ cutting-edge AI models including Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4 – with transparent pricing that matches provider rates exactly.
3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action:
diff --git a/bun.lock b/bun.lock
index eeaccd2363..d381043d01 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,6 @@
{
"lockfileVersion": 1,
- "configVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "@kilocode/kilo",
@@ -27,7 +27,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -79,7 +79,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -112,7 +112,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -142,7 +142,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -171,7 +171,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@ai-sdk/anthropic": "2.0.65",
"@ai-sdk/openai": "2.0.101",
@@ -206,7 +206,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
- "version": "7.1.23",
+ "version": "7.2.3",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -219,7 +219,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -239,7 +239,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/util": "workspace:*",
@@ -274,7 +274,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -327,7 +327,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
- "version": "7.1.23",
+ "version": "7.2.3",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -451,7 +451,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"zod": "catalog:",
@@ -465,14 +465,14 @@
},
"packages/script": {
"name": "@opencode-ai/script",
- "version": "7.1.23",
+ "version": "7.2.3",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
- "version": "7.1.23",
+ "version": "7.2.3",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -483,7 +483,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
- "version": "7.1.23",
+ "version": "7.2.3",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -506,7 +506,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -553,7 +553,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "7.1.23",
+ "version": "7.2.3",
"dependencies": {
"zod": "catalog:",
},
diff --git a/package.json b/package.json
index 7c626177b8..58d70dd493 100644
--- a/package.json
+++ b/package.json
@@ -122,6 +122,6 @@
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
},
- "version": "7.1.23",
+ "version": "7.2.3",
"peerDependencies": {}
}
diff --git a/packages/app/package.json b/packages/app/package.json
index 9ea7ae3152..f975e0dcc8 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "7.1.23",
+ "version": "7.2.3",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json
index 1bd4786dac..4cc72842bb 100644
--- a/packages/desktop-electron/package.json
+++ b/packages/desktop-electron/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 83a34dc878..b74f408b65 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index e2b800dc95..94cf6fade3 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
-version = "7.1.23"
+version = "7.2.3"
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.23/opencode-darwin-arm64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.3/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.23/opencode-darwin-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.3/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.23/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.3/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.23/opencode-linux-x64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.3/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.23/opencode-windows-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.3/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/kilo-docs/components/ThemeToggle.tsx b/packages/kilo-docs/components/ThemeToggle.tsx
index 24a21c9cc9..e73ae55955 100644
--- a/packages/kilo-docs/components/ThemeToggle.tsx
+++ b/packages/kilo-docs/components/ThemeToggle.tsx
@@ -58,19 +58,19 @@ export function ThemeToggle() {
if (!mounted) {
return (
)
}
const getIcon = () => {
if (theme === "system") {
- return "💻"
+ return
}
if (theme === "dark") {
- return "🌙"
+ return
}
- return "☀️"
+ return
}
const getLabel = () => {
@@ -86,7 +86,7 @@ export function ThemeToggle() {
return (
<>
>
)
}
+
+function SunIcon() {
+ return (
+
+ )
+}
+
+function MoonIcon() {
+ return (
+
+ )
+}
+
+function SystemIcon() {
+ return (
+
+ )
+}
diff --git a/packages/kilo-docs/lib/nav/code-with-ai.ts b/packages/kilo-docs/lib/nav/code-with-ai.ts
index 69a90dc778..e1d7b8f039 100644
--- a/packages/kilo-docs/lib/nav/code-with-ai.ts
+++ b/packages/kilo-docs/lib/nav/code-with-ai.ts
@@ -14,7 +14,11 @@ export const CodeWithAiNav: NavSection[] = [
href: "/code-with-ai/platforms/jetbrains",
children: "JetBrains Extension",
},
- { href: "/code-with-ai/platforms/cli", children: "CLI" },
+ {
+ href: "/code-with-ai/platforms/cli",
+ children: "CLI",
+ subLinks: [{ href: "/code-with-ai/platforms/cli-reference", children: "Command Reference" }],
+ },
{ href: "/code-with-ai/platforms/cloud-agent", children: "Cloud Agent" },
{ href: "/code-with-ai/platforms/mobile", children: "Mobile Apps" },
{ href: "/code-with-ai/platforms/slack", children: "Slack" },
@@ -45,10 +49,6 @@ export const CodeWithAiNav: NavSection[] = [
children: "Custom Models",
platform: "new",
},
- {
- href: "/code-with-ai/agents/free-and-budget-models",
- children: "Free & Budget Models",
- },
{
href: "/code-with-ai/agents/using-agents",
children: "Agents",
diff --git a/packages/kilo-docs/mappingplan.md b/packages/kilo-docs/mappingplan.md
index ce3a63b9b2..f61bd750bf 100644
--- a/packages/kilo-docs/mappingplan.md
+++ b/packages/kilo-docs/mappingplan.md
@@ -33,7 +33,6 @@
| Using Modes | `basic-usage/using-modes` |
| Orchestrator Mode | `basic-usage/orchestrator-mode` |
| Model Selection | `basic-usage/model-selection-guide` |
-| Free & Budget Models | `advanced-usage/free-and-budget-models` |
| **Features** (subheader) | |
| Autocomplete | `basic-usage/autocomplete/index`, `basic-usage/autocomplete/mistral-setup` |
| Code Actions | `features/code-actions` |
diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md
new file mode 100644
index 0000000000..d6437cac49
--- /dev/null
+++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md
@@ -0,0 +1,25 @@
+
+
+| Command | Description |
+| --- | --- |
+| `kilo acp` | start ACP (Agent Client Protocol) server |
+| `kilo mcp` | manage MCP (Model Context Protocol) servers |
+| `kilo [project]` | start kilo tui |
+| `kilo attach ` | attach to a running kilo server |
+| `kilo run [message..]` | run kilo with a message |
+| `kilo debug` | debugging and troubleshooting tools |
+| `kilo auth` | manage credentials |
+| `kilo agent` | manage agents |
+| `kilo upgrade [target]` | upgrade kilo to the latest or a specific version |
+| `kilo uninstall` | uninstall kilo and remove all related files |
+| `kilo serve` | starts a headless kilo server |
+| `kilo models [provider]` | list all available models |
+| `kilo stats` | show token usage and cost statistics |
+| `kilo export [sessionID]` | export session data as JSON |
+| `kilo import ` | import session data from JSON file or URL |
+| `kilo pr ` | fetch and checkout a GitHub PR branch, then run kilo |
+| `kilo session` | manage sessions |
+| `kilo remote` | enable remote connection for real-time session relay |
+| `kilo db` | database tools |
+| `kilo help [command]` | show full CLI reference |
+| `kilo completion` | generate shell completion script |
diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json
index f3b83f7590..88fa3e9578 100644
--- a/packages/kilo-docs/package.json
+++ b/packages/kilo-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
- "version": "7.1.23",
+ "version": "7.2.3",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
diff --git a/packages/kilo-docs/pages/automate/agent-manager.md b/packages/kilo-docs/pages/automate/agent-manager.md
index 5e2c539e6a..c06f9f2309 100644
--- a/packages/kilo-docs/pages/automate/agent-manager.md
+++ b/packages/kilo-docs/pages/automate/agent-manager.md
@@ -31,6 +31,17 @@ The panel opens as an editor tab and stays active across focus changes.
Each Agent Manager session runs in an isolated git worktree on a separate branch, keeping your main branch clean.
+### PR Status Badges
+
+Worktree items in the sidebar display a **PR status badge** when the branch has an associated pull request:
+
+- **Open** — badge indicating the PR is open (its color can also reflect review and check status)
+- **Merged** — purple badge indicating the PR has been merged
+- **Closed** — red badge indicating the PR was closed without merging
+- **Draft** — gray badge indicating the PR is in draft state
+
+The badge appears on the right side of each worktree item and updates automatically via polling. If the worktree's branch doesn't have a PR yet, no badge is shown.
+
### Creating a New Worktree Session
1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to create a new worktree
diff --git a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md
index 69ec4bdc5f..a7d3fe5aab 100644
--- a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md
+++ b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md
@@ -132,4 +132,4 @@ Auto Model is actively being improved. We'd love to hear how it's working for yo
- [Model Selection Guide](/docs/code-with-ai/agents/model-selection) - General guidance on choosing models
- [Using Agents](/docs/code-with-ai/agents/using-agents) - Learn about different Kilo Code agents
-- [Free & Budget Models](/docs/code-with-ai/agents/free-and-budget-models) - Cost-effective alternatives
+- [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) - Cost-effective alternatives
diff --git a/packages/kilo-docs/pages/code-with-ai/agents/free-and-budget-models.md b/packages/kilo-docs/pages/code-with-ai/agents/free-and-budget-models.md
deleted file mode 100644
index ce24af58ff..0000000000
--- a/packages/kilo-docs/pages/code-with-ai/agents/free-and-budget-models.md
+++ /dev/null
@@ -1,290 +0,0 @@
----
-title: Free and Budget Models
-description: Learn how to use Kilo Code effectively while minimizing or eliminating costs through free models, budget-friendly alternatives, and smart usage strategies.
----
-
-# Free and Budget Models
-
-**Why this matters:** AI model costs can add up quickly during development. This guide shows you how to use Kilo Code effectively while minimizing or eliminating costs through free models, budget-friendly alternatives, and smart usage strategies.
-
-## Completely Free Options
-
-### Kilo Gateway Free Models
-
-From time to time, Kilo works with AI inference providers to offer free models. These are available through the Kilo Gateway. Currently, we are offering these free models:
-
-- **MiniMax M2.1 (free)** - A capable model from MiniMax with strong general-purpose performance.
-- **Z.AI: GLM 4.7 (free)** - Latest variant of the GLM family, purpose-built for agent-centric applications.
-- **MoonshotAI: Kimi K2.5 (free)** - Optimized for agentic capabilities, including advanced tool use, reasoning, and code synthesis.
-- **Giga Potato (free)** - A stealth release model that is free in its evaluation period.
-- **Arcee AI: Trinity Large Preview (free)** - A preview model from Arcee AI with strong capabilities.
-
-### OpenRouter Free Tier Models
-
-OpenRouter offers several models with generous free tiers. **Note:** You'll need to create a free OpenRouter account to access these models.
-
-**Setup:**
-
-1. Create a free [OpenRouter account](https://openrouter.ai)
-2. Get your API key from the dashboard
-3. Configure Kilo Code with the OpenRouter provider
-
-**Available free models:**
-
-- **Qwen3 Coder (free)** - Optimized for agentic coding tasks such as function calling, tool use, and long-context reasoning over repositories.
-- **Z.AI: GLM 4.5 Air (free)** - Lightweight variant of the GLM-4.5 family, purpose-built for agent-centric applications.
-- **DeepSeek: R1 0528 (free)** - Performance on par with OpenAI o1, but open-sourced and with fully open reasoning tokens.
-- **MoonshotAI: Kimi K2 (free)** - Optimized for agentic capabilities, including advanced tool use, reasoning, and code synthesis.
-
-## Cost-Effective Premium Models
-
-When you need more capability than free models provide, these options deliver excellent value:
-
-### Ultra-Budget Champions (Under $0.50 per million tokens)
-
-**Mistral Devstral Small**
-
-- **Cost:** ~$0.20 per million input tokens
-- **Best for:** Code generation, debugging, refactoring
-- **Performance:** 85% of premium model capability at 10% of the cost
-
-**Llama 4 Maverick**
-
-- **Cost:** ~$0.30 per million input tokens
-- **Best for:** Complex reasoning, architecture planning
-- **Performance:** Excellent for most development tasks
-
-**DeepSeek v3**
-
-- **Cost:** ~$0.27 per million input tokens
-- **Best for:** Code analysis, large codebase understanding
-- **Performance:** Strong technical reasoning
-
-### Mid-Range Value Models ($0.50-$2.00 per million tokens)
-
-**Qwen3 235B**
-
-- **Cost:** ~$1.20 per million input tokens
-- **Best for:** Complex projects requiring high accuracy
-- **Performance:** Near-premium quality at 40% of the cost
-
-## Smart Usage Strategies
-
-### The 50% Rule
-
-**Principle:** Use budget models for 50% of your tasks, premium models for the other 50%.
-
-**Budget model tasks:**
-
-- Code reviews and analysis
-- Documentation writing
-- Simple bug fixes
-- Boilerplate generation
-- Refactoring existing code
-
-**Premium model tasks:**
-
-- Complex architecture decisions
-- Debugging difficult issues
-- Performance optimization
-- New feature design
-- Critical production code
-
-### Context Management for Cost Savings
-
-**Minimize context size:**
-
-```typescript
-// Instead of mentioning entire files
-@src/components/UserProfile.tsx
-
-// Mention specific functions or sections
-@src/components/UserProfile.tsx:45-67
-```
-
-**Reuse context effectively:**
-
-- Keep key project notes in your repository (e.g., a AGENTS.md or docs folder)
-- Reduces need to re-explain project details
-- Saves tokens per conversation
-
-**Strategic file mentions:**
-
-- Only include files directly relevant to the task
-- Use [`@folder/`](/docs/code-with-ai/agents/context-mentions) for broad context, specific files for targeted work
-
-### Model Switching Strategies
-
-**Start cheap, escalate when needed:**
-
-1. **Begin with free models** (Qwen3 Coder, GLM-4.5-Air)
-2. **Switch to budget models** if free models struggle
-3. **Escalate to premium models** only for complex tasks
-
-**Use API Configuration Profiles:**
-
-- Set up [multiple profiles](/docs/ai-providers) for different cost tiers
-- Quick switching between free, budget, and premium models
-- Match model capability to task complexity
-
-### Mode-Based Cost Optimization
-
-**Use appropriate modes to limit expensive operations:**
-
-- **[Ask Agent](/docs/code-with-ai/agents/using-agents#ask):** Information gathering without code changes
-- **[Plan Agent](/docs/code-with-ai/agents/using-agents#plan):** Planning without expensive file operations
-- **[Debug Agent](/docs/code-with-ai/agents/using-agents#debug):** Focused troubleshooting
-
-**Custom modes for budget control:**
-
-- Create modes that restrict expensive tools
-- Limit file access to specific directories
-- Control which operations are auto-approved
-
-## Real-World Performance Comparisons
-
-### Code Generation Tasks
-
-**Simple function creation:**
-
-- **Mistral Devstral Small:** 95% success rate
-- **GPT-4:** 98% success rate
-- **Cost difference:** Free vs $0.20 vs $30 per million tokens
-
-**Complex refactoring:**
-
-- **Budget models:** 70-80% success rate
-- **Premium models:** 90-95% success rate
-- **Recommendation:** Start with budget, escalate if needed
-
-### Debugging Performance
-
-**Simple bugs:**
-
-- **Free models:** Usually sufficient
-- **Budget models:** Excellent performance
-- **Premium models:** Overkill for most cases
-
-**Complex system issues:**
-
-- **Free models:** 40-60% success rate
-- **Budget models:** 60-80% success rate
-- **Premium models:** 85-95% success rate
-
-## Hybrid Approach Recommendations
-
-### Daily Development Workflow
-
-**Morning planning session:**
-
-- Use **Architect mode** with **DeepSeek R1**
-- Plan features and architecture
-- Create task breakdowns
-
-**Implementation phase:**
-
-- Use **Code mode** with **budget models**
-- Generate and modify code
-- Handle routine development tasks
-
-**Complex problem solving:**
-
-- Switch to **premium models** when stuck
-- Use for critical debugging
-- Architecture decisions affecting multiple systems
-
-### Project Phase Strategy
-
-**Early development:**
-
-- Free and budget models for prototyping
-- Rapid iteration without cost concerns
-- Establish patterns and structure
-
-**Production preparation:**
-
-- Premium models for critical code review
-- Performance optimization
-- Security considerations
-
-## Cost Monitoring and Control
-
-### Track Your Usage
-
-**Monitor credit consumption:**
-
-- Check cost estimates in chat history
-- Review monthly usage patterns
-- Identify high-cost operations
-
-**Set spending limits:**
-
-- Use provider billing alerts
-- Configure [provider rate limits](/docs/ai-providers) to control usage
-- Set daily/monthly budgets
-
-### Cost-Saving Tips
-
-**Reduce system prompt size:**
-
-- [Disable MCP](/docs/automate/mcp/using-in-kilo-code) if not using external tools
-- Use focused custom modes
-- Minimize unnecessary context
-
-**Optimize conversation length:**
-
-- Use [Checkpoints](/docs/code-with-ai/features/checkpoints) to reset context
-- Start fresh conversations for unrelated tasks
-- Archive completed work
-
-**Batch similar tasks:**
-
-- Group related code changes
-- Handle multiple files in single requests
-- Reduce conversation overhead
-
-## Getting Started with Budget Models
-
-### Quick Setup Guide
-
-1. **Create OpenRouter account** for free models
-2. **Configure multiple providers** in Kilo Code
-3. **Set up API Configuration Profiles** for easy switching
-4. **Escalate to budget models** when needed
-5. **Reserve premium models** for complex work
-
-### Recommended Provider Mix
-
-**Free tier foundation:**
-
-- [OpenRouter](/docs/ai-providers/openrouter) - Free models
-- [Groq](/docs/ai-providers/groq) - Fast inference for supported models
-- [Z.ai](https://z.ai/model-api) - Provides a free model GLM-4.5-Flash
-
-**Budget tier options:**
-
-- [DeepSeek](/docs/ai-providers/deepseek) - Excellent value models
-- [Mistral](/docs/ai-providers/mistral) - Specialized coding models
-
-**Premium tier backup:**
-
-- [Anthropic](/docs/ai-providers/anthropic) - Claude for complex reasoning
-- [OpenAI](/docs/ai-providers/openai) - GPT-4 for critical tasks
-
-## Measuring Success
-
-**Track these metrics:**
-
-- Monthly AI costs vs. development productivity
-- Task completion rates by model tier
-- Time saved vs. money spent
-- Code quality improvements
-
-**Success indicators:**
-
-- 70%+ of tasks completed with free/budget models
-- Monthly costs under your target budget
-- Maintained or improved code quality
-- Faster development cycles
-
-By combining free models, strategic budget model usage, and smart optimization techniques, you can harness the full power of AI-assisted development while keeping costs minimal. Start with free options and gradually incorporate budget models as your needs and comfort with costs grow.
diff --git a/packages/kilo-docs/pages/code-with-ai/index.md b/packages/kilo-docs/pages/code-with-ai/index.md
index 2e229d00fd..9ec66e7591 100644
--- a/packages/kilo-docs/pages/code-with-ai/index.md
+++ b/packages/kilo-docs/pages/code-with-ai/index.md
@@ -38,7 +38,6 @@ Kilo uses specialized agents to help with different tasks:
- [**Model Selection**](/docs/code-with-ai/agents/model-selection) — Choose the right AI model for each task
- [**Context Mentions**](/docs/code-with-ai/agents/context-mentions) — Reference files, functions, and symbols
- [**Orchestrator Mode**](/docs/code-with-ai/agents/orchestrator-mode) — Legacy orchestration (now built into all agents)
-- [**Free & Budget Models**](/docs/code-with-ai/agents/free-and-budget-models) — Cost-effective AI options
## Features
diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md
new file mode 100644
index 0000000000..ed944d4407
--- /dev/null
+++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md
@@ -0,0 +1,816 @@
+---
+title: "CLI Command Reference"
+description: "Complete reference for all Kilo CLI commands and subcommands"
+---
+
+# CLI Command Reference
+
+
+
+## kilo acp
+
+```
+start ACP (Agent Client Protocol) server
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --port port to listen on [number] [default: 0]
+ --hostname hostname to listen on [string] [default: "127.0.0.1"]
+ --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false]
+ --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"]
+ --cors additional domains to allow for CORS [array] [default: []]
+ --cwd working directory [string] [default: "."]
+```
+
+## kilo mcp
+
+```
+manage MCP (Model Context Protocol) servers
+
+Commands:
+ kilo mcp add add an MCP server
+ kilo mcp list list MCP servers and their status [aliases: ls]
+ kilo mcp auth [name] authenticate with an OAuth-enabled MCP server
+ kilo mcp logout [name] remove OAuth credentials for an MCP server
+ kilo mcp debug debug OAuth connection for an MCP server
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp add
+
+```
+add an MCP server
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp list
+
+```
+list MCP servers and their status
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp auth
+
+```
+authenticate with an OAuth-enabled MCP server
+
+Commands:
+ kilo mcp auth list list OAuth-capable MCP servers and their auth status [aliases: ls]
+
+Positionals:
+ name name of the MCP server [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp auth list
+
+```
+list OAuth-capable MCP servers and their auth status
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp logout
+
+```
+remove OAuth credentials for an MCP server
+
+Positionals:
+ name name of the MCP server [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo mcp debug
+
+```
+debug OAuth connection for an MCP server
+
+Positionals:
+ name name of the MCP server [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo [project]
+
+```
+start kilo tui
+
+Positionals:
+ project path to start kilo in [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --port port to listen on [number] [default: 0]
+ --hostname hostname to listen on [string] [default: "127.0.0.1"]
+ --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false]
+ --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"]
+ --cors additional domains to allow for CORS [array] [default: []]
+ -m, --model model to use in the format of provider/model [string]
+ -c, --continue continue the last session [boolean]
+ -s, --session session id to continue [string]
+ --fork fork the session when continuing (use with --continue or --session) [boolean]
+ --cloud-fork fetch session from cloud and continue locally (use with --session) [boolean]
+ --prompt prompt to use [string]
+ --agent agent to use [string]
+```
+
+## kilo attach
+
+```
+attach to a running kilo server
+
+Positionals:
+ url http://localhost:4096 [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --dir directory to run in [string]
+ -c, --continue continue the last session [boolean]
+ -s, --session session id to continue [string]
+ --fork fork the session when continuing (use with --continue or --session) [boolean]
+ --cloud-fork fetch session from cloud and continue locally (use with --session) [boolean]
+ -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string]
+```
+
+## kilo run
+
+```
+run kilo with a message
+
+Positionals:
+ message message to send [string] [default: []]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --command the command to run, use message for args [string]
+ -c, --continue continue the last session [boolean]
+ -s, --session session id to continue [string]
+ --fork fork the session before continuing (requires --continue or --session) [boolean]
+ --cloud-fork fetch session from cloud and continue locally (requires --session) [boolean]
+ --share share the session [boolean]
+ -m, --model model to use in the format of provider/model [string]
+ --agent agent to use [string]
+ --format format: default (formatted) or json (raw JSON events) [string] [choices: "default", "json"] [default: "default"]
+ -f, --file file(s) to attach to message [array]
+ --title title for the session (uses truncated prompt if no value provided) [string]
+ --attach attach to a running opencode server (e.g., http://localhost:4096) [string]
+ -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string]
+ --dir directory to run in, path on remote server if attaching [string]
+ --port port for the local server (defaults to random port if no value provided) [number]
+ --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string]
+ --thinking show thinking blocks [boolean] [default: false]
+ --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false]
+```
+
+## kilo debug
+
+```
+debugging and troubleshooting tools
+
+Commands:
+ kilo debug config show resolved configuration
+ kilo debug lsp LSP debugging utilities
+ kilo debug rg ripgrep debugging utilities
+ kilo debug file file system debugging utilities
+ kilo debug scrap list all known projects
+ kilo debug skill list all available skills
+ kilo debug snapshot snapshot debugging utilities
+ kilo debug agent show agent configuration details
+ kilo debug paths show global paths (data, config, cache, state)
+ kilo debug wait wait indefinitely (for debugging)
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug config
+
+```
+show resolved configuration
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug lsp
+
+```
+LSP debugging utilities
+
+Commands:
+ kilo debug lsp diagnostics get diagnostics for a file
+ kilo debug lsp symbols search workspace symbols
+ kilo debug lsp document-symbols get symbols from a document
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug lsp diagnostics
+
+```
+get diagnostics for a file
+
+Positionals:
+ file [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug lsp symbols
+
+```
+search workspace symbols
+
+Positionals:
+ query [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug lsp document-symbols
+
+```
+get symbols from a document
+
+Positionals:
+ uri [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug rg
+
+```
+ripgrep debugging utilities
+
+Commands:
+ kilo debug rg tree show file tree using ripgrep
+ kilo debug rg files list files using ripgrep
+ kilo debug rg search search file contents using ripgrep
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug rg tree
+
+```
+show file tree using ripgrep
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --limit [number]
+```
+
+### kilo debug rg files
+
+```
+list files using ripgrep
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --query Filter files by query [string]
+ --glob Glob pattern to match files [string]
+ --limit Limit number of results [number]
+```
+
+### kilo debug rg search
+
+```
+search file contents using ripgrep
+
+Positionals:
+ pattern Search pattern [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --glob File glob patterns [array]
+ --limit Limit number of results [number]
+```
+
+### kilo debug file
+
+```
+file system debugging utilities
+
+Commands:
+ kilo debug file read read file contents as JSON
+ kilo debug file status show file status information
+ kilo debug file list list files in a directory
+ kilo debug file search search files by query
+ kilo debug file tree [dir] show directory tree
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug file read
+
+```
+read file contents as JSON
+
+Positionals:
+ path File path to read [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug file status
+
+```
+show file status information
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug file list
+
+```
+list files in a directory
+
+Positionals:
+ path File path to list [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug file search
+
+```
+search files by query
+
+Positionals:
+ query Search query [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug file tree
+
+```
+show directory tree
+
+Positionals:
+ dir Directory to tree [string] [default: "."]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug scrap
+
+```
+list all known projects
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug skill
+
+```
+list all available skills
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug snapshot
+
+```
+snapshot debugging utilities
+
+Commands:
+ kilo debug snapshot track track current snapshot state
+ kilo debug snapshot patch show patch for a snapshot hash
+ kilo debug snapshot diff show diff for a snapshot hash
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug snapshot track
+
+```
+track current snapshot state
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug snapshot patch
+
+```
+show patch for a snapshot hash
+
+Positionals:
+ hash hash [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug snapshot diff
+
+```
+show diff for a snapshot hash
+
+Positionals:
+ hash hash [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug agent
+
+```
+show agent configuration details
+
+Positionals:
+ name Agent name [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --tool Tool id to execute [string]
+ --params Tool params as JSON or a JS object literal [string]
+```
+
+### kilo debug paths
+
+```
+show global paths (data, config, cache, state)
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo debug wait
+
+```
+wait indefinitely (for debugging)
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo auth
+
+```
+manage credentials
+
+Commands:
+ kilo auth login [url] log in to a provider
+ kilo auth logout log out from a configured provider
+ kilo auth list list providers [aliases: ls]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo auth login
+
+```
+log in to a provider
+
+Positionals:
+ url kilo auth provider [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ -p, --provider provider id or name to log in to (skips provider selection) [string]
+ -m, --method login method label (skips method selection) [string]
+```
+
+### kilo auth logout
+
+```
+log out from a configured provider
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo auth list
+
+```
+list providers
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo agent
+
+```
+manage agents
+
+Commands:
+ kilo agent create create a new agent
+ kilo agent list list all available agents
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo agent create
+
+```
+create a new agent
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --path directory path to generate the agent file [string]
+ --description what the agent should do [string]
+ --mode agent mode [string] [choices: "all", "primary", "subagent"]
+ --tools comma-separated list of tools to enable (default: all). Available: "bash, read, write, edit, list, glob, grep, webfetch, task, todowrite, todoread" [string]
+ -m, --model model to use in the format of provider/model [string]
+```
+
+### kilo agent list
+
+```
+list all available agents
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo upgrade
+
+```
+upgrade kilo to the latest or a specific version
+
+Positionals:
+ target version to upgrade to, for ex '0.1.48' or 'v0.1.48' [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ -m, --method installation method to use [string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]
+```
+
+## kilo uninstall
+
+```
+uninstall kilo and remove all related files
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ -c, --keep-config keep configuration files [boolean] [default: false]
+ -d, --keep-data keep session data and snapshots [boolean] [default: false]
+ --dry-run show what would be removed without removing [boolean] [default: false]
+ -f, --force skip confirmation prompts [boolean] [default: false]
+```
+
+## kilo serve
+
+```
+starts a headless kilo server
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --port port to listen on [number] [default: 0]
+ --hostname hostname to listen on [string] [default: "127.0.0.1"]
+ --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false]
+ --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"]
+ --cors additional domains to allow for CORS [array] [default: []]
+```
+
+## kilo models
+
+```
+list all available models
+
+Positionals:
+ provider provider ID to filter models by [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --verbose use more verbose model output (includes metadata like costs) [boolean]
+ --refresh refresh the models cache from models.dev [boolean]
+```
+
+## kilo stats
+
+```
+show token usage and cost statistics
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --days show stats for the last N days (default: all time) [number]
+ --tools number of tools to show (default: all) [number]
+ --models show model statistics (default: hidden). Pass a number to show top N, otherwise shows all
+ --project filter by project (default: all projects, empty string: current project) [string]
+```
+
+## kilo export
+
+```
+export session data as JSON
+
+Positionals:
+ sessionID session id to export [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo import
+
+```
+import session data from JSON file or URL
+
+Positionals:
+ file path to JSON file or share URL [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo pr
+
+```
+fetch and checkout a GitHub PR branch, then run kilo
+
+Positionals:
+ number PR number to checkout [number]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo session
+
+```
+manage sessions
+
+Commands:
+ kilo session list list sessions
+ kilo session delete delete a session
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo session list
+
+```
+list sessions
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ -n, --max-count limit to N most recent sessions [number]
+ --format output format [string] [choices: "table", "json"] [default: "table"]
+```
+
+### kilo session delete
+
+```
+delete a session
+
+Positionals:
+ sessionID session ID to delete [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo remote
+
+```
+enable remote connection for real-time session relay
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo db
+
+```
+database tools
+
+Commands:
+ kilo db [query] open an interactive sqlite3 shell or run a query [default]
+ kilo db path print the database path
+ kilo db migrate migrate JSON data to SQLite (merges with existing data)
+
+Positionals:
+ query SQL query to execute [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --format Output format [string] [choices: "json", "tsv"] [default: "tsv"]
+```
+
+### kilo db path
+
+```
+print the database path
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+### kilo db migrate
+
+```
+migrate JSON data to SQLite (merges with existing data)
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+```
+
+## kilo help
+
+```
+show full CLI reference
+
+Positionals:
+ command command to show help for [string]
+
+Options:
+ --help Show help [boolean]
+ --version Show version number [boolean]
+ --all show help for all commands [boolean] [default: false]
+ --format output format [string] [choices: "md", "text"] [default: "md"]
+```
diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
index 1ee5cbfe61..ed6ff494c9 100644
--- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
+++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
@@ -61,27 +61,9 @@ Or use npm:
### Top-Level CLI Commands
-| Command | Description |
-| ------------------------- | ------------------------------------------ |
-| `kilo [project]` | Start the TUI (Terminal User Interface) |
-| `kilo run [message..]` | Run with a message (non-interactive mode) |
-| `kilo attach ` | Attach to a running kilo server |
-| `kilo serve` | Start a headless server |
-| `kilo web` | Start server and open web interface |
-| `kilo auth` | Manage credentials (login, logout, list) |
-| `kilo agent` | Manage agents (create, list) |
-| `kilo mcp` | Manage MCP servers (list, add, auth) |
-| `kilo models [provider]` | List available models |
-| `kilo stats` | Show token usage and cost statistics |
-| `kilo session` | Manage sessions (list) |
-| `kilo export [sessionID]` | Export session data as JSON |
-| `kilo import ` | Import session data from JSON file or URL |
-| `kilo upgrade [target]` | Upgrade kilo to latest or specific version |
-| `kilo uninstall` | Uninstall kilo and remove related files |
-| `kilo pr ` | Fetch and checkout a GitHub PR branch |
-| `kilo github` | Manage GitHub agent (install, run) |
-| `kilo debug` | Debugging and troubleshooting tools |
-| `kilo completion` | Generate shell completion script |
+{% partial file="cli-commands-table.md" /%}
+
+For detailed help on every command and subcommand, see the [CLI Command Reference](/docs/code-with-ai/platforms/cli-reference).
### Global Options
@@ -139,10 +121,11 @@ Or use npm:
#### Kilo Gateway Commands (when connected)
-| Command | Aliases | Description |
-| ---------- | ------------------------ | --------------------------------- |
-| `/profile` | `/me`, `/whoami` | View your Kilo Gateway profile |
-| `/teams` | `/team`, `/org`, `/orgs` | Switch between Kilo Gateway teams |
+| Command | Aliases | Description |
+| ---------- | ------------------------ | ----------------------------------------- |
+| `/profile` | `/me`, `/whoami` | View your Kilo Gateway profile |
+| `/teams` | `/team`, `/org`, `/orgs` | Switch between Kilo Gateway teams |
+| `/remote` | - | Toggle remote mode for Cloud Agent access |
#### Built-in Commands
@@ -460,6 +443,44 @@ kilo --continue
- Cannot be used with a prompt argument
- Only works when there's at least one previous session in the workspace
+## Remote Connections
+
+Remote Connections let you access your local CLI sessions from the Cloud Agents web interface. Requires [Kilo Gateway](/docs/gateway) connection.
+
+### Enabling Remote Mode
+
+**Toggle during a session:**
+
+```
+/remote
+```
+
+Requires connection to Kilo Gateway. The `/remote` command appears only when authenticated.
+
+**Enable by default:**
+
+Add to `~/.config/kilo/config.json`:
+
+```json
+{
+ "remote_control": true
+}
+```
+
+### Using Remote Mode
+
+Once enabled, start a CLI session and open [Cloud Agents](https://app.kilo.ai/cloud). Your local session appears in the dashboard. See [Cloud Agent Remote Connections](/docs/code-with-ai/platforms/cloud-agent#remote-connections) for details.
+
+### Requirements
+
+- Connection to Kilo Gateway
+- Same Kilo account on CLI and Cloud Agent
+- CLI must remain running with internet connection
+
+{% callout type="warning" title="Security Warning" %}
+Anyone with access to your Kilo account can send messages to your computer when remote mode is enabled.
+{% /callout %}
+
## Environment Variable Overrides
The CLI supports overriding config values with environment variables. The supported environment variables are:
@@ -482,6 +503,6 @@ Your selection is persisted locally so it carries over to future sessions.
There is no `--org` or `--team` flag on `kilo run`. Instead, the organization is determined from the following sources, in order of priority (highest first):
-1. **`KILO_ORG_ID` environment variable** — Best for non-interactive and CI environments.
+1. **`KILO_ORG_ID` environment variable** — Best for non-interactive and CI environments.
2. **`Persisted selection from the last `/teams` pick`** — If you've run an interactive session and selected an organization via `/teams`, that selection is stored in the CLI auth file and reused automatically.
diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md
index fc511e0e22..4e0f2a18bb 100644
--- a/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md
+++ b/packages/kilo-docs/pages/code-with-ai/platforms/cloud-agent.md
@@ -102,6 +102,33 @@ Cloud Agents support project-level [skills](/docs/code-with-ai/platforms/cli#ski
Global skills (`~/.kilocode/skills/`) are not available in Cloud Agents since there is no persistent user home directory.
{% /callout %}
+## Remote Connections
+
+Remote Connections let you access and control local CLI sessions from the Cloud Agents web interface. Your computer handles the compute; the cloud gives you a window into it from any device.
+
+### How It Works
+
+When remote mode is enabled in the CLI, your active local sessions appear in the Cloud Agents dashboard alongside cloud sessions. The connection is two-way:
+
+- **Messages and responses** sync in real-time
+- **Agent questions** appear in both places — answer wherever you are
+- **Permission requests** route to your active connection
+- **Full editing capabilities** work remotely
+
+### Enabling Remote Mode
+
+Remote mode must be enabled from the CLI. See [CLI Remote Connections](/docs/code-with-ai/platforms/cli#remote-connections) for setup instructions.
+
+### Requirements
+
+- Same Kilo account on both CLI and Cloud Agent
+- Active internet connection on the local machine
+- CLI must remain running
+
+{% callout type="warning" title="Security Warning" %}
+Anyone with access to your Kilo account can send messages to your computer when remote mode is enabled.
+{% /callout %}
+
## Perfect For
Cloud Agents are great for:
diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md
index 408d4e6965..1ed95d5f19 100644
--- a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md
+++ b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md
@@ -37,7 +37,13 @@ See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actio
### Is the context progress graph still available?
-The context progress graph will be [added soon](https://github.com/Kilo-Org/kilocode/issues/8210) for users who like to see it.
+Yes — the context progress graph (also known as the task timeline) is now available. It appears at the top of the chat panel and shows:
+
+- **Timeline bars** — colored bars representing session activity (different colors for read, write, tool, error, and text parts)
+- **Context window progress** — a three-segment bar showing used, reserved, and available tokens, with a visual indicator when usage exceeds 50%
+- **Token breakdown** — input, output, cache writes, and cache reads display
+
+You can expand or collapse the graph — your preference is saved in the `kilo-code.new.showTaskTimeline` setting.
### I like to closely monitor and approve the behavior of the agent. How can I do that better in the new version?
diff --git a/packages/kilo-docs/pages/getting-started/using-kilo-for-free.md b/packages/kilo-docs/pages/getting-started/using-kilo-for-free.md
index 4f6a70d9f6..00a189c0eb 100644
--- a/packages/kilo-docs/pages/getting-started/using-kilo-for-free.md
+++ b/packages/kilo-docs/pages/getting-started/using-kilo-for-free.md
@@ -1,92 +1,77 @@
---
title: "Using Kilo for Free"
-description: "Learn how to use Kilo Code without spending money by configuring free models for agentic tasks, autocomplete, and CLI background tasks"
+description: "How to use Kilo Code for free — Auto Model Free, finding free models, free autocomplete, and free background tasks"
---
# Using Kilo for Free
-Kilo Code can be used completely free of charge, but you need to understand where Kilo uses AI models and configure each one appropriately.
+Kilo Code can be used completely free of charge. There are three places where Kilo uses AI model inference, and each can be configured to use free models.
-## When Kilo Uses Model Inference
+## Where Kilo Uses Models
-Kilo uses AI model inference in three places:
+1. **Agentic interactions** — Conversations with coding agents in IDE extensions (VS Code, JetBrains), CLI, and cloud services like App Builder and Code Reviewer
+2. **Autocomplete** — In-editor code completions as you type (IDE extensions only)
+3. **Background tasks** — Automatic session titles and context summarization
-1. **Agentic interactions** - Coding assistant conversations in IDE extensions (VS Code, JetBrains), CLI, and cloud services like App Builder and Code Reviewer
-2. **Autocomplete** - In-editor code completions as you type (IDE extensions only)
-3. **CLI Background tasks** - Automatic session titles and context summarization (CLI only)
-
-Each of these can consume credits by default. **For a completely free Kilo experience, you must configure all three to use free models.**
+Each of these consumes credits by default. **To use Kilo entirely for free, configure all three to use free models.**
## Free Agentic Usage
-Kilo Code provides access to [free models](/docs/code-with-ai/agents/free-and-budget-models) for your coding tasks through the Kilo Gateway and partner providers.
+Kilo provides free models for coding tasks through the Kilo Gateway and partner providers.
-### Finding Free Models
+### Auto Model Free
-Free models are clearly labeled in the model picker across all Kilo platforms. To find and use them:
+The easiest way to get started is [**Auto Model Free**](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`). This is a Kilo-provided model tier that automatically routes your requests to the best available free models — no configuration needed.
+
+### Finding Other Free Models
+
+You can also browse and select individual free models. In the model picker, type `free` to filter the list — free models are clearly labeled across all platforms.
**In the IDE Extensions (VS Code, JetBrains):**
1. Click on the current model below the chat window
-2. Browse the model list—free models are labeled as "(free)"
-3. Select your preferred free model
+2. Type `free` in the search box
+3. Select any model labeled "(free)"
**In the CLI:**
-1. Open the CLI by running `kilo`
-2. Use the `/models` command to browse available models
-3. Free models are labeled as "free"
-4. Select a free model for your tasks
+1. Run `kilo` to open the CLI
+2. Use the `/models` command
+3. Type `free` to filter the list
-### Free Models for Cloud Tasks
+{% callout type="note" %}
+Some free models may be rate limited by the upstream provider. If you hit a rate limit, try switching to a different free model.
+{% /callout %}
-Kilo's cloud services—including App Builder, Code Reviewer, and other cloud-based features—also support free models. When configuring a cloud task:
+### Cloud Tasks
-1. Look for the model selection dropdown
-2. Free models are labeled as "(free)" in the dropdown
-3. Select any free model to avoid using credits
+Kilo's cloud services — App Builder, Code Reviewer, and others — also support free models. Select any model labeled "(free)" in the model dropdown when configuring a cloud task.
{% callout type="tip" %}
-The available free models change over time as Kilo partners with different AI inference providers. Check our [free and budget models guide](/docs/code-with-ai/agents/free-and-budget-models) for the latest options, and subscribe to our blog or join our Discord for updates.
+Available free models change over time as Kilo partners with different inference providers. Subscribe to our blog or join our [Discord](https://kilo.ai/discord) for updates.
{% /callout %}
## Free Autocomplete
-Kilo Code's autocomplete feature provides AI-powered code completions as you type in the IDE extensions.
+Kilo's autocomplete feature provides AI-powered code completions as you type in IDE extensions.
-### Default Behavior
-
-By default, autocomplete is routed through the Kilo Code provider and uses credits from your account.
-
-### If You Don't Have Credits
-
-If you run out of credits and haven't configured a free alternative, autocomplete will stop working. Your main coding workflow won't be affected -- you just won't get AI-powered completions.
+By default, autocomplete routes through the Kilo provider and uses credits. If you run out of credits without a free alternative configured, autocomplete stops working — but your main coding workflow is unaffected.
### How to Get It Free
-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.
+Add your own Mistral AI (Codestral) API key via **BYOK (Bring Your Own Key)** on the Kilo Gateway. Mistral offers a free tier for Codestral. When you configure a BYOK key, autocomplete requests use your key directly — at no cost on your Kilo balance.
-For step-by-step instructions, see our [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup).
+See the [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup) for step-by-step instructions.
-## Free CLI Background Tasks
+## Free Background Tasks
-The Kilo CLI uses AI in the background for quality-of-life features that enhance your experience like context compression and titling sessions.
+Kilo uses a small model in the background for tasks like session titling. By default this is Kilo Auto Small, which consumes credits. If the small model is unavailable, Kilo falls back to your primary model — which may also consume credits if it's a paid model.
-### Default Behavior
+To avoid credit usage for background tasks, set the small model to a free model:
-By default, CLI background tasks use `gpt-5-nano`, which consumes credits.
+**In the VS Code extension:** Go to **Settings → Models** and change the small model to any free model.
-### If You Don't Have Credits
-
-Background tasks degrade gracefully when you don't have credits:
-
-- **Session titles** fall back to truncating your first message instead of generating a smart summary
-- **Context management** uses simple truncation instead of intelligent summarization
-- **Your main workflow continues uninterrupted** - these are convenience features, not requirements
-
-### How to Get It Free
-
-Configure the `small_model` parameter in `~/.config/kilo/config.json` to use a free model:
+**In the CLI:** Set the `small_model` parameter in `~/.config/kilo/config.json`:
```json
{
@@ -94,11 +79,11 @@ Configure the `small_model` parameter in `~/.config/kilo/config.json` to use a f
}
```
-Replace `your-preferred-free-model` with any free model available in the model picker.
+Replace `your-preferred-free-model` with any free model from the model picker.
## 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 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
+- [Auto Model](/docs/code-with-ai/agents/auto-model) — Smart model routing including the free tier
+- [Mistral Setup Guide](/docs/code-with-ai/features/autocomplete/mistral-setup) — Free autocomplete via BYOK
+- [Autocomplete](/docs/code-with-ai/features/autocomplete) — Full autocomplete documentation
+- [CLI Documentation](/docs/code-with-ai/platforms/cli) — Complete CLI reference
diff --git a/packages/kilo-docs/pages/kiloclaw/chat-platforms/slack.md b/packages/kilo-docs/pages/kiloclaw/chat-platforms/slack.md
index 4826ef7c3f..e0e35b525c 100644
--- a/packages/kilo-docs/pages/kiloclaw/chat-platforms/slack.md
+++ b/packages/kilo-docs/pages/kiloclaw/chat-platforms/slack.md
@@ -47,10 +47,7 @@ You need two tokens from Slack:
## Step 4: Pair Slack with KiloClaw
-1. In Slack, DM the app and type your slash command (e.g., `/claw`) followed by anything — this triggers the pairing flow
-
-> 📝 **Note**
-> The slash command is whatever you defined in the manifest. Any text after the command will work to trigger pairing.
+1. In Slack, DM the app and send any message — this triggers the pairing flow
2. The app will return a pairing code
3. Return to [app.kilocode.ai/claw](https://app.kilocode.ai/claw) and confirm the pairing code and approve
diff --git a/packages/kilo-docs/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js
index c6d3804f43..f20702d05f 100644
--- a/packages/kilo-docs/previous-docs-redirects.js
+++ b/packages/kilo-docs/previous-docs-redirects.js
@@ -691,7 +691,13 @@ module.exports = [
},
{
source: "/docs/advanced-usage/free-and-budget-models",
- destination: "/docs/code-with-ai/agents/free-and-budget-models",
+ destination: "/docs/getting-started/using-kilo-for-free",
+ basePath: false,
+ permanent: true,
+ },
+ {
+ source: "/docs/code-with-ai/agents/free-and-budget-models",
+ destination: "/docs/getting-started/using-kilo-for-free",
basePath: false,
permanent: true,
},
diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json
index ffca4a9415..5691509af8 100644
--- a/packages/kilo-gateway/package.json
+++ b/packages/kilo-gateway/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json
index 83091e5838..18c75ae505 100644
--- a/packages/kilo-i18n/package.json
+++ b/packages/kilo-i18n/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json
index 6bed2eac5d..046f52041c 100644
--- a/packages/kilo-telemetry/package.json
+++ b/packages/kilo-telemetry/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
diff --git a/packages/kilo-telemetry/src/events.ts b/packages/kilo-telemetry/src/events.ts
index ddfde6a9a8..c621da8dc0 100644
--- a/packages/kilo-telemetry/src/events.ts
+++ b/packages/kilo-telemetry/src/events.ts
@@ -25,6 +25,9 @@ export enum TelemetryEvent {
MCP_SERVER_CONNECTED = "MCP Server Connected",
MCP_SERVER_ERROR = "MCP Server Error",
+ // Remote Events
+ REMOTE_CONNECTION_OPENED = "Remote Connection Opened",
+
// Auth Events
AUTH_SUCCESS = "Auth Success",
AUTH_LOGOUT = "Auth Logout",
diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts
index 5b353aadca..5e4379892d 100644
--- a/packages/kilo-telemetry/src/telemetry.ts
+++ b/packages/kilo-telemetry/src/telemetry.ts
@@ -184,6 +184,11 @@ export namespace Telemetry {
track(TelemetryEvent.MCP_SERVER_ERROR, { server, error })
}
+ // Remote
+ export function trackRemoteConnectionOpened() {
+ track(TelemetryEvent.REMOTE_CONNECTION_OPENED)
+ }
+
// Auth
export function trackAuthSuccess(provider: string) {
track(TelemetryEvent.AUTH_SUCCESS, { provider })
diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json
index 327a1083c7..4f99390c20 100644
--- a/packages/kilo-ui/package.json
+++ b/packages/kilo-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
- "version": "7.1.23",
+ "version": "7.2.3",
"type": "module",
"license": "MIT",
"exports": {
diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx
index a96af21fe1..66c8176cbc 100644
--- a/packages/kilo-ui/src/components/diff.tsx
+++ b/packages/kilo-ui/src/components/diff.tsx
@@ -142,6 +142,24 @@ export function Diff(props: DiffProps) {
host.removeAttribute("data-color-scheme")
}
+ // Patch a bug in @pierre/diffs where `grid-template-columns: 100% auto` is set
+ // for `line-info-basic` separators under `@media (pointer: fine)`, causing the
+ // expand button to consume 100% of the gutter width and overlap the separator
+ // content text. We inject into `@layer unsafe` which overrides `@layer base`.
+ let separatorPatchSheet: CSSStyleSheet | null = null
+ const patchSeparatorLayout = () => {
+ const root = getRoot()
+ if (!root) return
+ if (!separatorPatchSheet) {
+ separatorPatchSheet = new CSSStyleSheet()
+ separatorPatchSheet.replaceSync(
+ `@layer unsafe { @media (pointer: fine) { [data-separator='line-info-basic'][data-expand-index] [data-separator-wrapper] { grid-template-columns: 34px auto; } } }`,
+ )
+ }
+ if (!root.adoptedStyleSheets.includes(separatorPatchSheet))
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, separatorPatchSheet]
+ }
+
const lineIndex = (split: boolean, element: HTMLElement) => {
const raw = element.dataset.lineIndex
if (!raw) return
@@ -576,6 +594,7 @@ export function Diff(props: DiffProps) {
})
applyScheme()
+ patchSeparatorLayout()
setRendered((value) => value + 1)
notifyRendered()
diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css
index 307ae9fccd..802bea6c7f 100644
--- a/packages/kilo-ui/src/components/message-part.css
+++ b/packages/kilo-ui/src/components/message-part.css
@@ -19,6 +19,18 @@
height: 20px;
}
}
+
+ [data-slot="assistant-copy-wrapper"] {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-top: 2px;
+
+ [data-component="icon-button"] {
+ width: 20px;
+ height: 20px;
+ }
+ }
}
/* Prevent long title/path from hiding the collapsible expand arrow */
diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx
index 187a6a2df2..41388c8622 100644
--- a/packages/kilo-ui/src/components/message-part.tsx
+++ b/packages/kilo-ui/src/components/message-part.tsx
@@ -949,6 +949,7 @@ export function Part(props: MessagePartProps) {
export interface ToolProps {
input: Record
metadata: Record
+ partMetadata?: Record
tool: string
partID?: string
callID?: string
@@ -1060,7 +1061,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const input = () => part.state?.input ?? emptyInput
// @ts-expect-error
- const partMetadata = () => part.state?.metadata ?? emptyMetadata
+ const meta = () => part.state?.metadata ?? emptyMetadata
+ const top = () => part.metadata ?? emptyMetadata
const render = createMemo(() => ToolRegistry.render(part.tool) ?? McpTool)
@@ -1122,7 +1124,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
tool={part.tool}
partID={part.id}
callID={part.callID}
- metadata={partMetadata()}
+ metadata={meta()}
+ partMetadata={top()}
// @ts-expect-error
output={part.state.output}
status={part.state.status}
@@ -1155,6 +1158,7 @@ PART_MAPPING["compaction"] = function CompactionPartDisplay() {
PART_MAPPING["text"] = function TextPartDisplay(props) {
const data = useData()
+ const i18n = useI18n()
const part = () => props.part as TextPart
const displayText = () => (part().text ?? "").trim()
@@ -1166,6 +1170,21 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
return props.turnDiffSummary
})
+ const showCopy = createMemo(() => {
+ if (props.message.role !== "assistant") return false
+ if (props.showAssistantCopyPartID === null) return false
+ return props.showAssistantCopyPartID === part().id
+ })
+ const [copied, setCopied] = createSignal(false)
+
+ const handleCopy = async () => {
+ const content = displayText()
+ if (!content) return
+ await navigator.clipboard.writeText(content)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
const handleMarkdownClick = (e: MouseEvent) => {
if (!data.openFile) return
const target = e.target
@@ -1200,6 +1219,24 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
+
{(render) => (
@@ -1220,6 +1257,9 @@ const streamed = new Set()
// Tracks parts that have already been auto-collapsed once, so component
// recreation (from store updates while other parts stream) won't collapse again.
const autocollapsed = new Set()
+// Tracks parts that the user has explicitly opened, so auto-collapse won't
+// override the user's intent when reasoning finishes or a tool call starts.
+const userOpened = new Set()
// Overrides upstream flat markdown render with streaming reasoning block + auto-collapse.
// Also filters encrypted reasoning data from OpenRouter that appears as [REDACTED].
@@ -1249,14 +1289,24 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
// Streaming → open. Just finished (was streaming, now done) → open briefly
// then collapse. Historical → collapsed from the start.
- const [open, setOpen] = createSignal(!done() || was)
+ // Restore user's explicit open preference across component recreations.
+ const [open, setOpen] = createSignal(!done() || was || userOpened.has(id))
+
+ // Propagate user intent to the module-level set so it survives component
+ // recreations (e.g. when a tool call arrives while reading reasoning).
+ const track = (value: boolean) => {
+ if (value) userOpened.add(id)
+ else userOpened.delete(id)
+ setOpen(value)
+ }
// Auto-collapse once when reasoning finishes (streaming → done transition).
// Collapses immediately so the grid transition runs in sync with the
// streaming-height removal. Module-level Set prevents re-triggering on
- // component recreation or when the user manually reopens.
+ // component recreation. Skipped entirely if the user has explicitly opened
+ // the block, so reading is not interrupted by a subsequent tool call.
createEffect(() => {
- if (done() && open() && !autocollapsed.has(id)) {
+ if (done() && open() && !autocollapsed.has(id) && !userOpened.has(id)) {
autocollapsed.add(id)
setOpen(false)
}
@@ -1264,13 +1314,32 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
onCleanup(() => {
if (done()) streamed.delete(id)
+ // userOpened is intentionally NOT deleted here. The component recreates
+ // frequently while other parts stream (same as autocollapsed), so removing
+ // the entry on unmount would discard the user's explicit preference and
+ // re-collapse the block on the next remount.
})
- // Auto-scroll the content container while streaming
+ // Auto-scroll the content container while streaming.
+ // Use a plain mutable flag rather than checking dist inside the reactive
+ // effect: by the time the effect runs the DOM has already grown, so reading
+ // scrollHeight post-update incorrectly reports the user as scrolled away
+ // whenever a streaming chunk is > 10px tall.
let ref: HTMLDivElement | undefined
+ let scrolled = false
+
+ const onScroll = (e: Event) => {
+ const el = e.currentTarget as HTMLDivElement
+ if (el.scrollHeight - el.clientHeight - el.scrollTop < 10) scrolled = false
+ }
+
+ const onWheel = (e: WheelEvent) => {
+ if (e.deltaY < 0) scrolled = true
+ }
+
createEffect(() => {
display()
- if (!done() && ref) {
+ if (!done() && ref && !scrolled) {
ref.scrollTop = ref.scrollHeight
}
})
@@ -1278,7 +1347,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
return (
-
+
@@ -1287,7 +1356,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
-
+
diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx
index 6ea1f5d8ad..81fa4874be 100644
--- a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx
+++ b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx
@@ -201,6 +201,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
cleanup = undefined
}
+ lastScrollTop = undefined
scroll = el
if (!el) return
diff --git a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png
index ca56f4c8fa..db85480266 100644
--- a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png
+++ b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6b0f41c54aca88874a3c74151b77ef20f0f17fa9bb2f149c13adfaf4f48de286
-size 14702
+oid sha256:cc32d99eeff1cb3061caa4a75e3353e30e6373073bbd7ebf17048c917367e11a
+size 28005
diff --git a/packages/kilo-vscode/docs/features/file-attachments.md b/packages/kilo-vscode/docs/features/file-attachments.md
index 6c452631f0..b9e90c0208 100644
--- a/packages/kilo-vscode/docs/features/file-attachments.md
+++ b/packages/kilo-vscode/docs/features/file-attachments.md
@@ -5,6 +5,19 @@
Image attachments and `@file` path mentions work. Non-image file content attachments are missing.
+## Supported Image Types
+
+The following image formats are supported for paste and drag-and-drop:
+
+- PNG (`image/png`)
+- JPEG (`image/jpeg`)
+- GIF (`image/gif`)
+- WebP (`image/webp`)
+
+## Drag-and-Drop (Shift Required)
+
+VS Code disables webview pointer-events during drag operations so it can handle drops in the editor area. To drop images into the chat input, **hold Shift while dragging**. This re-enables the webview to receive drop events (VS Code 1.91+, see [microsoft/vscode#182449](https://github.com/microsoft/vscode/issues/182449)).
+
## Remaining Work
- Add a file attachment button to the chat input toolbar (paperclip icon or similar)
diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js
index 69b79df854..549e60910e 100644
--- a/packages/kilo-vscode/esbuild.js
+++ b/packages/kilo-vscode/esbuild.js
@@ -191,23 +191,34 @@ async function main() {
// Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components)
const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js")
+ // Build Diff Virtual webview (lightweight single-file diff for permission approval)
+ const diffVirtualCtx = await createBrowserWebviewContext("webview-ui/diff-virtual/index.tsx", "dist/diff-virtual.js")
+
// Build webview
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
if (watch) {
- await Promise.all([extensionCtx.watch(), webviewCtx.watch(), agentManagerCtx.watch(), diffViewerCtx.watch()])
+ await Promise.all([
+ extensionCtx.watch(),
+ webviewCtx.watch(),
+ agentManagerCtx.watch(),
+ diffViewerCtx.watch(),
+ diffVirtualCtx.watch(),
+ ])
} else {
await Promise.all([
extensionCtx.rebuild(),
webviewCtx.rebuild(),
agentManagerCtx.rebuild(),
diffViewerCtx.rebuild(),
+ diffVirtualCtx.rebuild(),
])
await Promise.all([
extensionCtx.dispose(),
webviewCtx.dispose(),
agentManagerCtx.dispose(),
diffViewerCtx.dispose(),
+ diffVirtualCtx.dispose(),
])
}
}
diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs
index f82b5a9d93..fe2a70ddb5 100644
--- a/packages/kilo-vscode/eslint.config.mjs
+++ b/packages/kilo-vscode/eslint.config.mjs
@@ -29,13 +29,76 @@ export default [
eqeqeq: "warn",
"no-throw-literal": "warn",
"max-lines": ["error", 3000],
+ complexity: ["error", 20],
},
},
+
+ // ── Complexity exceptions ─────────────────────────────────────────
+ // Existing violations capped at their current max.
+ // New code must stay ≤ 20. Do not raise these caps; refactor instead.
{
files: ["src/KiloProvider.ts"],
- rules: {
- "max-lines": ["error", 3200],
- },
+ rules: { complexity: ["error", 140], "max-lines": ["error", 3300] },
},
+ {
+ files: ["webview-ui/agent-manager/AgentManagerApp.tsx"],
+ rules: { complexity: ["error", 74], "max-lines": ["error", 3100] },
+ },
+ {
+ files: ["src/agent-manager/AgentManagerProvider.ts"],
+ rules: { complexity: ["error", 64] },
+ },
+ {
+ files: ["webview-ui/src/components/chat/PromptInput.tsx"],
+ rules: { complexity: ["error", 48] },
+ },
+ {
+ files: ["src/legacy-migration/migration-service.ts"],
+ rules: { complexity: ["error", 45] },
+ },
+ {
+ files: ["webview-ui/src/components/migration/MigrationWizard.tsx"],
+ rules: { complexity: ["error", 37] },
+ },
+ {
+ files: ["webview-ui/src/context/session.tsx"],
+ rules: { complexity: ["error", 31] },
+ },
+ {
+ files: ["src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts"],
+ rules: { complexity: ["error", 30] },
+ },
+ {
+ files: ["src/agent-manager/WorktreeManager.ts", "webview-ui/src/components/chat/QuestionDock.tsx"],
+ rules: { complexity: ["error", 28] },
+ },
+ {
+ files: [
+ "src/kilo-provider-utils.ts",
+ "src/services/autocomplete/continuedev/core/autocomplete/postprocessing/index.ts",
+ ],
+ rules: { complexity: ["error", 27] },
+ },
+ {
+ files: ["webview-ui/src/components/settings/CustomProviderDialog.tsx"],
+ rules: { complexity: ["error", 26] },
+ },
+ {
+ files: ["src/agent-manager/WorktreeStateManager.ts"],
+ rules: { complexity: ["error", 24] },
+ },
+ {
+ files: ["webview-ui/src/utils/errorUtils.ts"],
+ rules: { complexity: ["error", 23] },
+ },
+ {
+ files: ["src/services/autocomplete/continuedev/core/autocomplete/filtering/BracketMatchingService.ts"],
+ rules: { complexity: ["error", 22] },
+ },
+ {
+ files: ["webview-ui/src/context/server.tsx"],
+ rules: { complexity: ["error", 21] },
+ },
+
eslintConfigPrettier,
]
diff --git a/packages/kilo-vscode/knip.json b/packages/kilo-vscode/knip.json
index 806593bc57..3e71849562 100644
--- a/packages/kilo-vscode/knip.json
+++ b/packages/kilo-vscode/knip.json
@@ -4,6 +4,7 @@
"src/extension.ts",
"webview-ui/agent-manager/index.tsx",
"webview-ui/diff-viewer/index.tsx",
+ "webview-ui/diff-virtual/index.tsx",
"webview-ui/src/index.tsx",
"src/**/__tests__/**/*.{ts,spec.ts}",
"src/**/*.test.ts",
@@ -11,7 +12,6 @@
"script/*.ts"
],
"project": ["src/**/*.ts", "webview-ui/**/*.{ts,tsx}"],
- "ignore": ["src/services/autocomplete/**"],
"ignoreExportsUsedInFile": true,
"exclude": ["dependencies", "devDependencies", "optionalPeerDependencies", "unlisted", "unresolved", "binaries"]
}
diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json
index 44ef0219c8..bb4f4ef35a 100644
--- a/packages/kilo-vscode/package.json
+++ b/packages/kilo-vscode/package.json
@@ -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.23",
+ "version": "7.2.3",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
@@ -50,6 +50,17 @@
],
"main": "./dist/extension.js",
"contributes": {
+ "taskDefinitions": [
+ {
+ "type": "kilo-worktree-setup",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The setup script command to execute"
+ }
+ }
+ }
+ ],
"viewsContainers": {
"activitybar": [
{
diff --git a/packages/kilo-vscode/src/DiffViewerProvider.ts b/packages/kilo-vscode/src/DiffViewerProvider.ts
index f9f143f746..67466a0fa8 100644
--- a/packages/kilo-vscode/src/DiffViewerProvider.ts
+++ b/packages/kilo-vscode/src/DiffViewerProvider.ts
@@ -212,6 +212,7 @@ export class DiffViewerProvider implements vscode.Disposable {
public dispose(): void {
this.stopDiffPolling()
+ this.gitOps.dispose()
this.panel?.dispose()
this.outputChannel.dispose()
}
diff --git a/packages/kilo-vscode/src/DiffVirtualProvider.ts b/packages/kilo-vscode/src/DiffVirtualProvider.ts
new file mode 100644
index 0000000000..b585ee71fd
--- /dev/null
+++ b/packages/kilo-vscode/src/DiffVirtualProvider.ts
@@ -0,0 +1,107 @@
+import * as vscode from "vscode"
+import { buildWebviewHtml } from "./utils"
+import { appendOutput, getWorkspaceRoot } from "./review-utils"
+
+export interface DiffVirtualFile {
+ file: string
+ before: string
+ after: string
+ additions: number
+ deletions: number
+}
+
+/**
+ * DiffVirtualProvider opens a lightweight diff viewer for a single in-memory
+ * file diff (not backed by git). Used by the permission approval dock to show
+ * edit changes before the user approves or rejects them.
+ */
+export class DiffVirtualProvider implements vscode.Disposable {
+ private panel: vscode.WebviewPanel | undefined
+ private pending: DiffVirtualFile | undefined
+ private outputChannel: vscode.OutputChannel
+
+ constructor(private readonly extensionUri: vscode.Uri) {
+ this.outputChannel = vscode.window.createOutputChannel("Kilo Diff Virtual")
+ }
+
+ private log(...args: unknown[]) {
+ appendOutput(this.outputChannel, "DiffVirtual", ...args)
+ }
+
+ public open(diff: DiffVirtualFile): void {
+ this.pending = diff
+ const filename = diff.file.split("/").pop() ?? diff.file
+ const title = `Changes: ${filename}`
+
+ if (this.panel) {
+ this.panel.title = title
+ this.panel.reveal(vscode.ViewColumn.One)
+ this.pushData()
+ return
+ }
+
+ const panel = vscode.window.createWebviewPanel("kilo-code.new.DiffVirtualPanel", title, vscode.ViewColumn.One, {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: [this.extensionUri],
+ })
+
+ panel.iconPath = {
+ light: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-light.svg"),
+ dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
+ }
+
+ panel.webview.html = this.getHtml(panel.webview)
+ panel.webview.onDidReceiveMessage((msg) => this.onMessage(msg))
+ panel.onDidDispose(() => {
+ this.log("Panel disposed")
+ this.panel = undefined
+ this.pending = undefined
+ })
+
+ this.panel = panel
+ }
+
+ private onMessage(msg: Record): void {
+ const type = msg.type as string
+
+ if (type === "webviewReady") {
+ this.post({
+ type: "ready",
+ vscodeLanguage: vscode.env.language,
+ languageOverride: vscode.workspace.getConfiguration("kilo-code.new").get("language"),
+ workspaceDirectory: getWorkspaceRoot(),
+ })
+ this.pushData()
+ return
+ }
+
+ if (type === "diffVirtual.close") {
+ this.panel?.dispose()
+ }
+ }
+
+ private pushData(): void {
+ if (!this.pending) return
+ this.post({ type: "diffVirtual.data", diff: this.pending })
+ }
+
+ private post(message: Record): void {
+ if (this.panel?.webview) void this.panel.webview.postMessage(message)
+ }
+
+ private getHtml(webview: vscode.Webview): string {
+ return buildWebviewHtml(webview, {
+ scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.js")),
+ styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.css")),
+ iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
+ title: "Diff Virtual",
+ extraStyles: "#root { display: flex; flex-direction: column; height: 100%; }",
+ })
+ }
+
+ public dispose(): void {
+ this.panel?.dispose()
+ this.outputChannel.dispose()
+ }
+}
diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts
index 2b153315bb..23f90af0cf 100644
--- a/packages/kilo-vscode/src/KiloProvider.ts
+++ b/packages/kilo-vscode/src/KiloProvider.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines -- TODO: refactor to reduce file size and remove this disable */
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
@@ -35,11 +36,15 @@ import {
import { GitOps } from "./agent-manager/GitOps"
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
import { getWorkspaceRoot } from "./review-utils"
-import { MarketplaceService } from "./services/marketplace"
+import { MarketplaceService, type MarketplaceItem, type RemoveResult } from "./services/marketplace"
+import type { RemoteStatusService } from "./services/RemoteStatusService"
import { resolveProjectDirectory } from "./project-directory"
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
+import { retry } from "./services/cli-backend/retry"
import { slimPart, slimParts } from "./kilo-provider/slim-metadata"
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
+import { childID } from "./kilo-provider/task-session"
+import { retryable, backoff, MAX_RETRIES } from "./util/retry"
// legacy-migration start
import {
checkAndShowMigrationWizard,
@@ -88,14 +93,29 @@ import {
saveCustomProvider as saveCustomProviderAction,
} from "./provider-actions"
import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models"
+import type { Agent } from "@kilocode/sdk/v2/client"
type KiloProviderOptions = {
projectDirectory?: string | null
slimEditMetadata?: boolean
}
+// Helper to map agent data to the subset of fields sent to the webview
+const mapAgent = (a: Agent) => ({
+ name: a.name,
+ displayName: a.displayName,
+ description: a.description,
+ mode: a.mode,
+ native: a.native,
+ hidden: a.hidden,
+ color: a.color,
+ deprecated: a.deprecated,
+ permission: a.permission,
+})
+
export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider {
public static readonly viewType = "kilo-code.SidebarProvider"
+ private readonly instanceId = crypto.randomUUID()
private webview: vscode.Webview | null = null
private currentSession: Session | null = null
@@ -124,10 +144,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private cachedMcpStatusMessage: unknown = null
/** Ref-count of in-flight handleUpdateConfig calls; prevents fetchAndSendConfig from sending stale data */
private pending = 0
+ private configWarningsShown = false
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: unknown = null
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
private readyResolvers: (() => void)[] = []
+ private promptRecoveryQueued = false
+ private promptRecovery: Promise | null = null
private trackedSessionIds: Set = new Set()
private syncedChildSessions: Set = new Set()
/** Tracks the latest status for each session, used to warn before destructive config operations. */
@@ -156,6 +179,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private unsubscribeDirectoryProvider: (() => void) | null = null
private initConnectionPromise: Promise | null = null
private webviewMessageDisposable: vscode.Disposable | null = null
+ private viewStateDisposable: vscode.Disposable | null = null
+ private visibilityDisposable: vscode.Disposable | null = null
/** Lazily initialized ignore controller for .kilocodeignore filtering */
private ignoreController: FileIgnoreController | null = null
@@ -168,6 +193,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private pendingFollowup: Followup | null = null
/** Worktree diff stats poller for the sidebar badge — reuses GitStatsPoller (local stats only) */
private statsPoller: GitStatsPoller | null = null
+ private statsGitOps: GitOps | null = null
private cachedStats: unknown = null
/** Optional interceptor called before the standard message handler.
@@ -179,6 +205,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
| ((sessionId: string, progress: (status: string, detail?: string, error?: string) => void) => Promise)
| null = null
+ private diffVirtualProvider: import("./DiffVirtualProvider").DiffVirtualProvider | undefined
+ private remoteService: RemoteStatusService | null = null
+ private unsubscribeRemote: (() => void) | null = null
+
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
@@ -191,12 +221,29 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
TelemetryProxy.getInstance().setProvider(this)
}
+ setRemoteService(service: RemoteStatusService): void {
+ this.remoteService = service
+ this.unsubscribeRemote = service.onChange(() => this.sendRemoteStatus())
+ }
+ private sendRemoteStatus(): void {
+ const s = this.remoteService?.getState()
+ if (s) this.postMessage({ type: "remoteStatus", enabled: s.enabled, connected: s.connected })
+ }
+ private focusSession(id?: string): void {
+ if (id) this.connectionService.registerFocused(this.instanceId, id)
+ else this.connectionService.unregisterFocused(this.instanceId)
+ }
+
public setProjectDirectory(directory: string | null): void {
if (this.projectDirectory === directory) return
this.projectDirectory = directory
this.postMessage({ type: "workspaceDirectoryChanged", directory: directory ?? "" })
}
+ public setDiffVirtualProvider(provider: import("./DiffVirtualProvider").DiffVirtualProvider): void {
+ this.diffVirtualProvider = provider
+ }
+
getTelemetryProperties(): Record {
return {
appName: "kilo-code",
@@ -281,7 +328,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Use fire-and-forget (no throwOnError) to match old getProfile() which returned null on error.
if (this.connectionState === "connected" && this.client) {
console.log("[Kilo New] KiloProvider: 👤 syncWebviewState fetching profile...")
- const profileResult = await this.client.kilo.profile()
+ const profileResult = await retry(() => this.client!.kilo.profile())
const profileData = profileResult.data ?? null
console.log("[Kilo New] KiloProvider: 👤 syncWebviewState profile:", profileData ? "received" : "null")
this.postMessage({
@@ -300,6 +347,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// authoritative and reconciliation risks race-resetting busy sessions.
const reconcile = this.sessionStatusMap.size === 0
void this.seedSessionStatusMap(reconcile)
+
+ this.sendRemoteStatus()
}
// legacy-migration start
@@ -330,20 +379,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
localResourceRoots: [this.extensionUri],
}
- // Set HTML content
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview)
-
- // Handle messages from webview (shared handler)
this.setupWebviewMessageHandler(webviewView.webview)
- // Track sidebar visibility for keybinding when-clauses and stats polling
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
- webviewView.onDidChangeVisibility(() => {
+ this.visibilityDisposable?.dispose()
+ this.visibilityDisposable = webviewView.onDidChangeVisibility(() => {
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
this.statsPoller?.setEnabled(webviewView.visible)
+ this.focusSession(webviewView.visible ? this.currentSession?.id : undefined)
})
-
- // Initialize connection to CLI backend
this.initializeConnection()
}
@@ -362,9 +407,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
panel.webview.html = this._getHtmlForWebview(panel.webview)
- // Handle messages from webview (shared handler)
this.setupWebviewMessageHandler(panel.webview)
-
+ this.viewStateDisposable?.dispose()
+ this.viewStateDisposable = panel.onDidChangeViewState(() =>
+ this.focusSession(panel.active ? this.currentSession?.id : undefined),
+ )
this.initializeConnection()
}
@@ -420,6 +467,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
void this.handleLoadSessions()
}
+ /** Recover permission/question prompts after sessions and directories are tracked. */
+ public recoverPendingPrompts(): void {
+ this.promptRecoveryQueued = true
+ if (!this.isWebviewReady) return
+ if (!this.client) return
+ if (this.promptRecovery) return
+
+ this.promptRecovery = this.flushPendingPrompts().finally(() => {
+ this.promptRecovery = null
+ if (this.promptRecoveryQueued && this.isWebviewReady && this.client) this.recoverPendingPrompts()
+ })
+ }
+
+ private async flushPendingPrompts(): Promise {
+ while (this.promptRecoveryQueued && this.isWebviewReady) {
+ if (!this.client) return
+ this.promptRecoveryQueued = false
+ await Promise.all([
+ fetchAndSendPendingPermissions(this.permissionCtx),
+ fetchAndSendPendingQuestions(this.questionCtx),
+ ])
+ }
+ }
+
public openCloudSession(sessionId: string): void {
this.postMessage({ type: "openCloudSession", sessionId })
}
@@ -456,6 +527,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
*/
private setupWebviewMessageHandler(webview: vscode.Webview): void {
this.webviewMessageDisposable?.dispose()
+ // eslint-disable-next-line complexity -- TODO: refactor to reduce complexity and remove this disable
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
// Run interceptor if attached (e.g., AgentManagerProvider worktree logic)
if (this.onBeforeMessage) {
@@ -475,6 +547,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.isWebviewReady = true
await this.syncWebviewState("webviewReady")
this.flushPendingReviewComments()
+ this.recoverPendingPrompts()
this.readyResolvers.splice(0).forEach((r) => r())
break
case "sendMessage": {
@@ -529,6 +602,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
case "abort":
+ this.cancelRetry(message.sessionID ?? "")
await this.handleAbort(message.sessionID)
break
case "revertSession":
@@ -557,6 +631,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "clearSession":
this.contextSessionID = this.currentSession?.id ?? this.contextSessionID
this.currentSession = null
+ this.focusSession()
break
case "loadMessages":
// Don't await: allow parallel loads so rapid session switching
@@ -608,6 +683,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "openChanges":
vscode.commands.executeCommand("kilo-code.new.showChanges")
break
+ case "openDiffVirtual":
+ if (this.diffVirtualProvider && message.diff) {
+ this.diffVirtualProvider.open(message.diff)
+ }
+ break
case "continueInWorktree":
if (message.sessionId && this.continueInWorktreeHandler) {
this.continueInWorktreeHandler(message.sessionId, (status: string, detail?: string, error?: string) => {
@@ -787,6 +867,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "renameSession":
await this.handleRenameSession(message.sessionID, message.title)
break
+ case "toggleRemote":
+ case "setRemoteEnabled":
+ case "requestRemoteStatus":
+ this.remoteService
+ ?.handleMessage(message.type, message.enabled)
+ .then((s) => {
+ if (s) this.sendRemoteStatus()
+ })
+ .catch((err) => console.error("[Kilo New] remote message failed:", err))
+ break
case "updateSetting":
await this.handleUpdateSetting(message.key, message.value)
break
@@ -965,12 +1055,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
case "removeInstalledMarketplaceItem": {
- const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
- const result = await this.getMarketplace().remove(message.mpItem, scope, workspace)
- if (result.success) {
- await this.invalidateAfterMarketplaceChange(scope)
- }
+ const result = await this.removeMarketplaceItem(message.mpItem, scope)
this.postMessage({
type: "marketplaceRemoveResult",
success: result.success,
@@ -1022,6 +1108,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Subscribe to SSE events for this webview (filtered by tracked sessions)
this.unsubscribeEvent = this.connectionService.onEventFiltered(
(event) => {
+ // Remote status events are global and should always pass through
+ if (event.type === "kilo-sessions.remote-status-changed") return true
const sessionId = this.connectionService.resolveEventSessionId(event)
// message.part.updated and message.part.delta are always session-scoped; drop if session unknown.
@@ -1052,6 +1140,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.postMessage({ type: "connectionState", state })
if (state === "connected") {
+ // Fire config warnings independently so a failure in the
+ // sequential await chain doesn't prevent warnings from being shown
+ void this.checkConfigWarnings("state")
try {
// Profile fetch is best-effort — returns 401 when user isn't logged into gateway.
const sdkClient = this.client
@@ -1061,8 +1152,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)
+ this.recoverPendingPrompts()
} catch (error) {
console.error("[Kilo New] KiloProvider: ❌ Failed during connected state handling:", error)
this.postMessage({
@@ -1127,8 +1217,17 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
this.postMessage({ type: "connectionState", state: this.connectionState })
+
+ // connect() can resolve after SSE reaches "connected" but before this
+ // provider subscribes to onStateChange(). In that case the initial
+ // connected callback is missed, so run the warning check here too.
+ if (this.connectionState === "connected") {
+ void this.checkConfigWarnings("init")
+ }
+
await this.syncWebviewState("initializeConnection")
await this.flushPendingSessionRefresh("initializeConnection")
+ this.recoverPendingPrompts()
// Fetch providers, agents, skills, config, notifications, and session statuses in parallel
await Promise.all([
@@ -1142,6 +1241,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
])
this.sendNotificationSettings()
this.sendTimelineSetting()
+ this.postMessage({ type: "extensionDataReady" })
// Start polling worktree diff stats for the sidebar badge
this.startStatsPolling()
@@ -1206,6 +1306,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private async handleLoadMessages(sessionID: string): Promise {
// Track the session so we receive its SSE events
this.trackedSessionIds.add(sessionID)
+ this.focusSession(sessionID)
this.contextSessionID = sessionID
if (!this.client) {
@@ -1225,9 +1326,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getWorkspaceDirectory(sessionID)
- const { data: messagesData } = await this.client.session.messages(
- { sessionID, directory: workspaceDir },
- { throwOnError: true, signal: abort.signal },
+ const { data: messagesData } = await retry(() =>
+ this.client!.session.messages(
+ { sessionID, directory: workspaceDir },
+ { throwOnError: true, signal: abort.signal },
+ ),
)
// If this request was aborted while awaiting, skip posting stale results
@@ -1289,9 +1392,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
messages,
})
- // Recover any permission.asked events that were missed while the webview
- // was loading or during an SSE reconnection (fire-and-forget).
- void fetchAndSendPendingPermissions(this.permissionCtx)
+ // Recover any prompts missed while the webview was loading or during an SSE reconnection.
+ this.recoverPendingPrompts()
} catch (error) {
// Silently ignore aborted requests — the user switched to a different session
if (abort.signal.aborted) return
@@ -1328,9 +1430,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getWorkspaceDirectory(sessionID)
- const { data: messagesData } = await this.client.session.messages(
- { sessionID, directory: workspaceDir },
- { throwOnError: true },
+ const { data: messagesData } = await retry(() =>
+ this.client!.session.messages({ sessionID, directory: workspaceDir }, { throwOnError: true }),
)
const messages = messagesData.map((m) => ({
@@ -1349,11 +1450,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
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)
+ // Recover any prompts emitted by the child before we started tracking it.
+ this.recoverPendingPrompts()
} catch (err) {
this.syncedChildSessions.delete(sessionID)
console.error("[Kilo New] KiloProvider: Failed to sync child session:", err)
@@ -1600,21 +1698,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getWorkspaceDirectory()
- const { data: agents } = await this.client.app.agents({ directory: workspaceDir }, { throwOnError: true })
+ const { data: agents } = await retry(() =>
+ this.client!.app.agents({ directory: workspaceDir }, { throwOnError: true }),
+ )
const { visible, defaultAgent } = filterVisibleAgents(agents)
const message = {
type: "agentsLoaded",
- agents: visible.map((a) => ({
- name: a.name,
- displayName: a.displayName,
- description: a.description,
- mode: a.mode,
- native: a.native,
- color: a.color,
- deprecated: a.deprecated,
- })),
+ agents: visible.map(mapAgent),
+ allAgents: agents.map(mapAgent),
defaultAgent,
}
this.cachedAgentsMessage = message
@@ -1634,7 +1727,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getWorkspaceDirectory()
- const { data: skills } = await this.client.app.skills({ directory: workspaceDir }, { throwOnError: true })
+ const { data: skills } = await retry(() =>
+ this.client!.app.skills({ directory: workspaceDir }, { throwOnError: true }),
+ )
const message = {
type: "skillsLoaded",
@@ -1657,7 +1752,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const dir = this.getWorkspaceDirectory()
- const { data: commands } = await this.client.command.list({ directory: dir }, { throwOnError: true })
+ const { data: commands } = await retry(() =>
+ this.client!.command.list({ directory: dir }, { throwOnError: true }),
+ )
const message = {
type: "commandsLoaded",
@@ -1679,7 +1776,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (!this.client) return undefined
try {
const dir = this.getWorkspaceDirectory()
- const { data } = await this.client.app.skills({ directory: dir }, { throwOnError: true })
+ const { data } = await retry(() => this.client!.app.skills({ directory: dir }, { throwOnError: true }))
return data
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch CLI skills for marketplace:", error)
@@ -1724,59 +1821,87 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
*/
private async handleRemoveMode(name: string): Promise {
if (!this.client) return
- let removed = false
// 1. Try CLI removal (handles .md files and legacy .kilocodemodes)
try {
const dir = this.getWorkspaceDirectory()
const result = await this.client.kilocode.removeAgent({ name, directory: dir })
- if (!result.error) removed = true
+ if (!result.error) {
+ this.cachedAgentsMessage = null
+ await this.fetchAndSendAgents()
+ return
+ }
} catch {
// CLI removal failed — agent may be in kilo.json instead
}
// 2. Try removing from kilo.json (handles marketplace-installed modes)
- if (!removed) {
- const workspace = this.getProjectDirectory(this.currentSession?.id)
- const mp = this.getMarketplace()
- const stub = { id: name, type: "mode" as const, name, description: "", content: "" }
- const project = await mp.remove(stub, "project", workspace)
- const global = await mp.remove(stub, "global", workspace)
- if (project.success || global.success) {
- await this.disposeCliInstance("global")
- removed = true
- }
- }
-
+ const stub = { id: name, type: "mode" as const, name, description: "", content: "" }
+ const removed = await this.removeMarketplaceItemFromAllScopes(stub)
if (!removed) {
console.error("[Kilo New] KiloProvider: Failed to remove mode:", name)
}
-
- this.cachedAgentsMessage = null
- await this.fetchAndSendAgents()
}
private async handleRemoveMcp(name: string): Promise {
- const workspace = this.getProjectDirectory(this.currentSession?.id)
- const mp = this.getMarketplace()
+ // Remove from legacy files first so that the subsequent invalidation
+ // causes the CLI to re-read config without the legacy entry.
+ await this.removeLegacyMcp(name)
+
const stub = { id: name, type: "mcp" as const, name, description: "", url: "", content: "" }
-
- // Remove from both scopes — an MCP could exist in project, global, or both
- const project = await mp.remove(stub, "project", workspace)
- const global = await mp.remove(stub, "global", workspace)
-
- if (project.success || global.success) {
- // Use global scope when removed from global (or both) so the global
- // config cache is also invalidated; project scope is a subset.
- const scope = global.success ? "global" : "project"
- await this.disposeCliInstance(scope)
- this.cachedConfigMessage = null
- await this.fetchAndSendConfig()
- } else {
+ const removed = await this.removeMarketplaceItemFromAllScopes(stub)
+ if (!removed) {
console.error("[Kilo New] KiloProvider: Failed to remove MCP server:", name)
}
}
+ /**
+ * Remove an MCP server from legacy config files (.kilo/mcp.json, .kilocode/mcp.json,
+ * and the VS Code global storage mcp_settings.json). These files are read by the
+ * CLI-side McpMigrator and merged into config at the lowest precedence level.
+ * Returns true if the entry was found and removed from at least one file.
+ */
+ private async removeLegacyMcp(name: string): Promise {
+ const workspace = this.getProjectDirectory(this.currentSession?.id)
+ const files: vscode.Uri[] = []
+
+ // Project-level legacy files
+ if (workspace) {
+ files.push(vscode.Uri.file(path.join(workspace, ".kilo", "mcp.json")))
+ files.push(vscode.Uri.file(path.join(workspace, ".kilocode", "mcp.json")))
+ }
+
+ // Global legacy file (VS Code extension global storage)
+ const storage = this.extensionContext?.globalStorageUri
+ if (storage) {
+ files.push(vscode.Uri.joinPath(storage, "settings", "mcp_settings.json"))
+ }
+
+ let removed = false
+ for (const uri of files) {
+ const bytes = await vscode.workspace.fs.readFile(uri).then(
+ (b) => b,
+ () => null,
+ )
+ if (!bytes) continue
+
+ try {
+ const parsed = JSON.parse(Buffer.from(bytes).toString("utf8")) as Record
+ const servers = parsed.mcpServers as Record | undefined
+ if (!servers?.[name]) continue
+
+ delete servers[name]
+ const content = Buffer.from(JSON.stringify(parsed, null, 2), "utf8")
+ await vscode.workspace.fs.writeFile(uri, content)
+ removed = true
+ } catch (err) {
+ console.warn("[Kilo New] KiloProvider: Failed to remove legacy MCP from", uri.fsPath, err)
+ }
+ }
+
+ return removed
+ }
+
private async fetchAndSendMcpStatus(): Promise {
if (!this.client) {
if (this.cachedMcpStatusMessage) {
@@ -1787,7 +1912,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const directory = this.getWorkspaceDirectory()
- const { data } = await this.client.mcp.status({ directory })
+ const { data } = await retry(() => this.client!.mcp.status({ directory }))
if (data) {
const message = { type: "mcpStatusLoaded", status: data }
this.cachedMcpStatusMessage = message
@@ -1823,23 +1948,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
/**
- * Dispose the CLI backend instance so it re-reads config from disk.
- * Call after any marketplace install/remove that writes config files directly.
- * Global-scope changes need global.dispose() to also reset the global config cache.
+ * Remove a marketplace item from a single scope and invalidate CLI caches.
*/
- private async disposeCliInstance(scope: "project" | "global"): Promise {
- if (!this.client) return
- if (scope === "global") {
- await this.client.global.dispose().catch((e: unknown) => {
- console.warn("[Kilo New] global.dispose() after marketplace change failed:", e)
- })
+ private async removeMarketplaceItem(item: MarketplaceItem, scope: "project" | "global"): Promise {
+ const workspace = this.getProjectDirectory(this.currentSession?.id)
+ const result = await this.getMarketplace().remove(item, scope, workspace)
+ if (result.success) {
+ await this.invalidateAfterMarketplaceChange(scope)
}
- // Always dispose the per-project instance so it rebuilds state from
- // the (possibly updated) global + project config on the next request.
- const dir = this.getWorkspaceDirectory()
- await this.client.instance.dispose({ directory: dir }).catch((e: unknown) => {
- console.warn("[Kilo New] instance.dispose() after marketplace change failed:", e)
- })
+ return result
+ }
+
+ /**
+ * Remove a marketplace item from both project and global scopes.
+ * mp.remove returns success even when the entry doesn't exist (no-op),
+ * so we must attempt both scopes to cover dual-scope installations.
+ * Returns true if at least one scope removal succeeded.
+ */
+ private async removeMarketplaceItemFromAllScopes(item: MarketplaceItem): Promise {
+ const workspace = this.getProjectDirectory(this.currentSession?.id)
+ const mp = this.getMarketplace()
+ const project = await mp.remove(item, "project", workspace)
+ const global = await mp.remove(item, "global", workspace)
+
+ if (project.success || global.success) {
+ const scope = global.success ? "global" : "project"
+ await this.invalidateAfterMarketplaceChange(scope)
+ return true
+ }
+ return false
}
/**
@@ -1898,7 +2035,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getWorkspaceDirectory()
- const { data: config } = await this.client.config.get({ directory: workspaceDir }, { throwOnError: true })
+ const { data: config } = await retry(() =>
+ this.client!.config.get({ directory: workspaceDir }, { throwOnError: true }),
+ )
const message = {
type: "configLoaded",
@@ -1945,7 +2084,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (!this.client || this.connectionState !== "connected") return
try {
const dir = this.getWorkspaceDirectory()
- const { data: config } = await this.client.config.get({ directory: dir }, { throwOnError: true })
+ const { data: config } = await retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true }))
this.cachedConfigMessage = { type: "configLoaded", config }
this.postMessage({ type: "configUpdated", config })
} catch (error) {
@@ -1953,6 +2092,49 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
+ /**
+ * Fetch config warnings from the server and display a single consolidated
+ * VS Code warning with a "Show Details" action button.
+ * Only shown once per provider lifecycle (flag resets on dispose/re-create, not on SSE reconnect).
+ */
+ private async checkConfigWarnings(from: string): Promise {
+ if (this.configWarningsShown) {
+ console.log("[Kilo New] KiloProvider: config warnings already shown", { from })
+ return
+ }
+ if (!this.client) {
+ console.log("[Kilo New] KiloProvider: config warnings skipped (no client)", { from })
+ return
+ }
+ try {
+ const dir = this.getWorkspaceDirectory()
+ console.log("[Kilo New] KiloProvider: checking config warnings", { from, dir })
+ const result = await this.client.config.warnings({ directory: dir })
+ const list = result?.data ?? []
+ console.log("[Kilo New] KiloProvider: config warnings fetched", { from, count: list.length })
+ if (list.length === 0) return
+ this.configWarningsShown = true
+
+ const first = list[0]!
+ const summary = list.length === 1 ? first.message : `${first.message} (and ${list.length - 1} more)`
+ console.warn("[Kilo New] KiloProvider: showing config warnings", { from, count: list.length, path: first.path })
+
+ const action = await vscode.window.showWarningMessage(`Config: ${summary}`, "Show Details")
+ if (action === "Show Details") {
+ const lines = list.map((w) => {
+ const base = `${w.path}\n ${w.message}`
+ return w.detail ? `${base}\n ${w.detail}` : base
+ })
+ const channel = vscode.window.createOutputChannel("Kilo Config Warnings")
+ channel.clear()
+ channel.appendLine(lines.join("\n\n"))
+ channel.show()
+ }
+ } catch (err) {
+ console.warn("[Kilo New] KiloProvider: checkConfigWarnings failed:", { from, err })
+ }
+ }
+
/**
* Fetch Kilo news/notifications and send to webview.
* Uses the cached message pattern so the webview gets data immediately on refresh.
@@ -1978,7 +2160,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
try {
- const { data: all } = await this.client.kilo.notifications(undefined, { throwOnError: true })
+ const { data: all } = await retry(() => this.client!.kilo.notifications(undefined, { throwOnError: true }))
const notifications = all.filter((n) => !n.showIn || n.showIn.includes("extension"))
const existing = this.extensionContext?.globalState.get("kilo.dismissedNotificationIds", []) ?? []
const active = new Set(notifications.map((n) => n.id))
@@ -2093,7 +2275,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Config.state is reset by updateGlobal (via Instance.resetStateEntry) so
// config.get() returns fresh data without a full dispose cycle.
const dir = this.getWorkspaceDirectory()
- const { data: merged } = await this.client.config.get({ directory: dir }, { throwOnError: true })
+ const { data: merged } = await retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true }))
this.cachedConfigMessage = { type: "configLoaded", config: merged }
this.postMessage({ type: "configUpdated", config: merged })
@@ -2150,6 +2332,85 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return { sid, dir }
}
+ /** Abort controllers for active retry loops, keyed by session ID */
+ private retryAbortControllers = new Map()
+
+ /**
+ * Execute an SDK call with exponential backoff on HTTP errors.
+ * Retries on 429, 5xx, and other retryable status codes.
+ * When the response includes `Retry-After` / `Retry-After-MS` headers,
+ * the delay honours that value (capped at 5 min). Otherwise uses the
+ * predefined backoff schedule: 5s -> 10s -> 30s -> 60s -> 300s.
+ *
+ * After MAX_RETRIES (5) attempts, automatically throws the error.
+ * Users can cancel via the cancel button in the UI which sends an abort
+ * message — this interrupts the backoff delay and stops the retry loop.
+ *
+ * The webview receives `sessionStatus` messages with a countdown so the
+ * user can see that a retry is in progress.
+ */
+ private async withRetry(fn: () => Promise<{ error?: unknown; response: Response }>, sid: string): Promise {
+ const abortController = new AbortController()
+ this.retryAbortControllers.set(sid, abortController)
+
+ try {
+ for (let attempt = 1; ; attempt++) {
+ if (abortController.signal.aborted) {
+ // User cancelled — return normally without triggering sendMessageFailed
+ return
+ }
+
+ const result = await fn()
+ if (!result.error) return
+
+ const status = result.response?.status ?? 0
+
+ // Non-retryable status codes fail immediately without retry
+ if (!retryable(status)) {
+ this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
+ throw result.error
+ }
+
+ // Stop retrying after MAX_RETRIES attempts
+ if (attempt >= MAX_RETRIES) {
+ this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
+ throw result.error
+ }
+
+ const delay = backoff(attempt, result.response?.headers)
+ console.log(`[Kilo New] KiloProvider: Retry on ${status}, attempt ${attempt}/${MAX_RETRIES}, delay ${delay}ms`)
+
+ this.postMessage({
+ type: "sessionStatus",
+ sessionID: sid,
+ status: "retry",
+ attempt,
+ message: `Error (${status}). Retrying...`,
+ next: Date.now() + delay,
+ })
+
+ // Wait for delay or until aborted
+ await new Promise((resolve) => {
+ const timer = setTimeout(resolve, delay)
+ abortController.signal.addEventListener("abort", () => {
+ clearTimeout(timer)
+ })
+ })
+ }
+ } finally {
+ this.retryAbortControllers.delete(sid)
+ }
+ }
+
+ /** Cancel an active retry loop for a session */
+ private cancelRetry(sid: string): void {
+ const controller = this.retryAbortControllers.get(sid)
+ if (controller) {
+ controller.abort()
+ this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
+ }
+ }
+
private async handleSendMessage(
text: string,
messageID?: string,
@@ -2192,18 +2453,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.connectionService.recordMessageSessionId(messageID, resolved!.sid)
}
- await this.client.session.promptAsync(
- {
- sessionID: resolved!.sid,
- directory: resolved!.dir,
- messageID,
- parts,
- model: providerID && modelID ? { providerID, modelID } : undefined,
- agent,
- variant,
- editorContext,
- },
- { throwOnError: true },
+ const sid = resolved!.sid
+ const dir = resolved!.dir
+ await this.withRetry(
+ () =>
+ this.client!.session.promptAsync({
+ sessionID: sid,
+ directory: dir,
+ messageID,
+ parts,
+ model: providerID && modelID ? { providerID, modelID } : undefined,
+ agent,
+ variant,
+ editorContext,
+ }),
+ sid,
)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to send message:", error)
@@ -2254,19 +2518,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const parts = files?.map((f) => ({ type: "file" as const, mime: f.mime, url: f.url }))
- await this.client.session.command(
- {
- sessionID: resolved!.sid,
- directory: resolved!.dir,
- command,
- arguments: args,
- messageID,
- model: providerID && modelID ? `${providerID}/${modelID}` : undefined,
- agent,
- variant,
- parts,
- },
- { throwOnError: true },
+ const sid = resolved!.sid
+ const dir = resolved!.dir
+ await this.withRetry(
+ () =>
+ this.client!.session.command({
+ sessionID: sid,
+ directory: dir,
+ command,
+ arguments: args,
+ messageID,
+ model: providerID && modelID ? `${providerID}/${modelID}` : undefined,
+ agent,
+ variant,
+ parts,
+ }),
+ sid,
)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to send command:", error)
@@ -2614,6 +2881,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
* Filters events by project ID and tracked session IDs so each webview only sees its own sessions.
*/
private handleEvent(event: Event): void {
+ if (event.type === "kilo-sessions.remote-status-changed") {
+ this.remoteService?.updateFromEvent({ enabled: event.properties.enabled, connected: event.properties.connected })
+ return
+ }
+
// Drop session events from other projects before any tracking logic.
// This must come first: the trackedSessionIds guard below would otherwise
// let a foreign session through if it was accidentally tracked.
@@ -2691,9 +2963,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
type?: string
tool?: string
metadata?: { sessionId?: string }
+ state?: { metadata?: { sessionId?: string } }
sessionID?: string
}
- const childId = part.type === "tool" && part.tool === "task" ? part.metadata?.sessionId : undefined
+ const childId = childID(part)
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)
@@ -2996,7 +3269,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private startStatsPolling(): void {
this.statsPoller?.stop()
+ this.statsGitOps?.dispose()
const git = new GitOps({ log: () => {} })
+ this.statsGitOps = git
this.statsPoller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => getWorkspaceRoot(),
@@ -3023,7 +3298,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
* Does NOT kill the server — that's the connection service's job.
*/
dispose(): void {
+ this.unsubscribeRemote?.()
+ this.focusSession()
this.statsPoller?.stop()
+ this.statsGitOps?.dispose()
this.unsubscribeEvent?.()
this.unsubscribeState?.()
this.unsubscribeNotificationDismiss?.()
@@ -3033,7 +3311,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.unsubscribeMigrationComplete?.()
this.unsubscribeClearPendingPrompts?.()
this.unsubscribeDirectoryProvider?.()
+ this.viewStateDisposable?.dispose()
+ this.visibilityDisposable?.dispose()
this.webviewMessageDisposable?.dispose()
+ this.isWebviewReady = false
+ this.promptRecoveryQueued = false
this.trackedSessionIds.clear()
this.syncedChildSessions.clear()
this.sessionDirectories.clear()
diff --git a/packages/kilo-vscode/src/SettingsEditorProvider.ts b/packages/kilo-vscode/src/SettingsEditorProvider.ts
index 922754e319..493a6fd519 100644
--- a/packages/kilo-vscode/src/SettingsEditorProvider.ts
+++ b/packages/kilo-vscode/src/SettingsEditorProvider.ts
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
import { KiloProvider } from "./KiloProvider"
import { resolvePanelProjectDirectory } from "./project-directory"
import type { KiloConnectionService } from "./services/cli-backend"
+import type { RemoteStatusService } from "./services/RemoteStatusService"
type PanelView = "settings" | "profile" | "marketplace"
@@ -26,6 +27,7 @@ export class SettingsEditorProvider implements vscode.Disposable {
private panels = new Map()
private providers = new Map()
private tabs = new Map()
+ private remoteService: RemoteStatusService | null = null
constructor(
private readonly extensionUri: vscode.Uri,
@@ -101,6 +103,9 @@ export class SettingsEditorProvider implements vscode.Disposable {
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
projectDirectory,
})
+ if (this.remoteService) {
+ provider.setRemoteService(this.remoteService)
+ }
provider.resolveWebviewPanel(panel)
// Listen for closePanel from the webview (back button in panel mode)
@@ -144,6 +149,14 @@ export class SettingsEditorProvider implements vscode.Disposable {
})
}
+ setRemoteService(service: RemoteStatusService): void {
+ this.remoteService = service
+ // Apply to any existing providers
+ for (const [, provider] of this.providers) {
+ provider.setRemoteService(service)
+ }
+ }
+
dispose(): void {
for (const [, panel] of this.panels) {
panel.dispose()
diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
index 424d0b54b0..3442100587 100644
--- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
+++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
@@ -6,8 +6,10 @@ import { getErrorMessage } from "../kilo-provider-utils"
import { isAbsolutePath } from "../path-utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { WorktreeStateManager, remoteRef } from "./WorktreeStateManager"
+import { handleSection } from "./section-handler"
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
import { GitStatsPoller, type WorktreePresenceResult } from "./GitStatsPoller"
+import { PRStatusBridge } from "./pr-status-bridge"
import { GitOps, type ApplyConflict } from "./GitOps"
import { versionedName } from "./branch-name"
import { normalizePath, classifyWorktreeError } from "./git-import"
@@ -23,6 +25,7 @@ import { continueInWorktree } from "./continue-in-worktree"
import { shouldStopDiffPolling } from "./delete-worktree"
import { buildKeybindingMap } from "./format-keybinding"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
+import { Semaphore } from "./semaphore"
import { PLATFORM } from "./constants"
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
import { hashFileDiffs, resolveLocalDiffTarget } from "../review-utils"
@@ -53,6 +56,7 @@ export class AgentManagerProvider implements Disposable {
private diffSessionId: string | undefined
private lastDiffHash: string | undefined
private statsPoller: GitStatsPoller
+ private prBridge!: PRStatusBridge
private gitOps: GitOps
private cachedDiffTarget: { sessionId: string; directory: string; baseBranch: string } | undefined
private staleWorktreeIds = new Set()
@@ -73,11 +77,13 @@ export class AgentManagerProvider implements Disposable {
(msg) => this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
createTerminalHost(),
)
- this.gitOps = new GitOps({ log: (...args) => this.log(...args) })
+ const semaphore = new Semaphore(3)
+ this.gitOps = new GitOps({ log: (...args) => this.log(...args), semaphore })
this.statsPoller = new GitStatsPoller({
getWorktrees: () => this.state?.getWorktrees() ?? [],
getWorkspaceRoot: () => this.getRoot(),
getClient: () => this.connectionService.getClient(),
+ semaphore,
onStats: (stats) => {
const msg = { type: "agentManager.worktreeStats" as const, stats }
this.cachedWorktreeStats = msg
@@ -94,6 +100,16 @@ export class AgentManagerProvider implements Disposable {
log: (...args) => this.log(...args),
git: this.gitOps,
})
+ this.prBridge = PRStatusBridge.create({
+ getWorktrees: () => this.state?.getWorktrees() ?? [],
+ getWorkspaceRoot: () => this.getRoot(),
+ postToWebview: (m) => this.postToWebview(m),
+ updateWorktreePR: (id, n, u, s) => this.state?.updateWorktreePR(id, n, u, s),
+ hasPersistedPR: (id: string) => !!this.state?.getWorktree(id)?.prNumber,
+ openExternal: (u) => this.host.openExternal(u),
+ log: (...a) => this.log(...a),
+ semaphore,
+ })
}
private log(...args: unknown[]) {
@@ -147,13 +163,14 @@ export class AgentManagerProvider implements Disposable {
this.stateReady = this.initializeState()
void this.sendRepoInfo()
this.sendKeybindings()
-
+ this.prBridge.attachPanel(ctx)
ctx.onDidDispose(() => {
// Only clear if this is still the active panel — a newer panel may
// have already replaced us via attachPanel.
if (this.panel === ctx) {
this.log("Panel disposed")
this.statsPoller.stop()
+ this.prBridge.poller.stop()
this.stopDiffPolling()
this.panel = undefined
}
@@ -184,31 +201,34 @@ export class AgentManagerProvider implements Disposable {
this.host.refreshGit()
}
- // Do not auto-remove stale worktrees on load.
- // Presence checks run in the shared poller and require explicit user cleanup.
-
- // Register all worktree sessions with the session provider
- for (const worktree of state.getWorktrees()) {
- for (const session of state.getSessions(worktree.id)) {
- this.panel?.sessions.setSessionDirectory(session.id, worktree.path)
- this.panel?.sessions.trackSession(session.id)
+ for (const wt of state.getWorktrees()) {
+ for (const s of state.getSessions(wt.id)) {
+ this.panel?.sessions.setSessionDirectory(s.id, wt.path)
+ this.panel?.sessions.trackSession(s.id)
}
}
-
- // Push full state to webview
+ for (const s of state.getSessions()) if (!s.worktreeId) this.panel?.sessions.trackSession(s.id)
this.pushState()
// Refresh sessions so worktree sessions appear in the list
if (state.getSessions().length > 0) {
this.panel?.sessions.refreshSessions()
}
+
+ // Recover any pending permission/question prompts that were missed during
+ // panel recreation or SSE reconnection. Must run after all worktree sessions
+ // are registered with their directory overrides so the recovery queries the
+ // correct CLI backend Instances.
+ this.panel?.sessions.recoverPendingPrompts()
}
// ---------------------------------------------------------------------------
// Message interceptor
// ---------------------------------------------------------------------------
+ // eslint-disable-next-line complexity -- TODO: refactor to reduce complexity and remove this disable
private async onMessage(msg: Record): Promise | null> {
+ if (this.prBridge.handleMessage(msg)) return null
const m = msg as unknown as AgentManagerInMessage
if (m.type === "agentManager.createWorktree") {
@@ -218,8 +238,12 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
if (m.type === "agentManager.openLocally") {
- if (!this.panel) return null
- this.panel.sessions.clearSessionDirectory(m.sessionId)
+ this.panel?.sessions.clearSessionDirectory(m.sessionId)
+ const st = this.getStateManager()
+ if (st?.getSession(m.sessionId)) {
+ st.moveSession(m.sessionId, null)
+ this.pushState()
+ }
return null
}
if (m.type === "continueInWorktree") {
@@ -231,6 +255,15 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId)
if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId)
if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
+ if (m.type === "agentManager.persistSession" || m.type === "agentManager.forgetSession") {
+ const persist = m.type === "agentManager.persistSession"
+ void this.stateReady?.then(() => {
+ const st = this.getStateManager()
+ if (st)
+ persist ? !st.getSession(m.sessionId) && st.addSession(m.sessionId, null) : st.removeSession(m.sessionId)
+ })
+ return null
+ }
if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) {
this.activeSessionId = m.draftID
}
@@ -292,6 +325,7 @@ export class AgentManagerProvider implements Disposable {
// already emitted before the webview was ready to receive messages.
if (this.cachedWorktreeStats) this.postToWebview(this.cachedWorktreeStats)
if (this.cachedLocalStats) this.postToWebview(this.cachedLocalStats)
+ this.prBridge.replay()
// Refresh sessions after pushState so the webview's sessionsLoaded
// handler is guaranteed to be registered (requestState fires from
// onMount). Without this, the initial refreshSessions() in
@@ -327,6 +361,7 @@ export class AgentManagerProvider implements Disposable {
this.state?.setSessionsCollapsed(m.collapsed)
return null
}
+ if (this.handleSection(m)) return null
if (m.type === "agentManager.setReviewDiffStyle") {
this.state?.setReviewDiffStyle(m.style)
return null
@@ -376,10 +411,18 @@ export class AgentManagerProvider implements Disposable {
void this.onApplyWorktreeDiff(m.worktreeId, selectedFiles)
return null
}
+ if (m.type === "agentManager.revertWorktreeFile") {
+ void this.onRevertWorktreeFile(m.sessionId, m.file)
+ return null
+ }
if (m.type === "agentManager.startDiffWatch") {
this.startDiffPolling(m.sessionId)
return null
}
+ if (m.type === "agentManager.openSessions") {
+ this.connectionService.registerOpen("agent-manager", m.sessionIDs)
+ return null
+ }
if (m.type === "agentManager.stopDiffWatch") {
this.stopDiffPolling()
return null
@@ -407,12 +450,15 @@ export class AgentManagerProvider implements Disposable {
// uses the correct session even before the session provider's async session.get completes.
if (m.type === "loadMessages") {
this.activeSessionId = m.sessionID
+ this.connectionService.registerFocused("agent-manager", m.sessionID)
this.terminalManager.syncOnSessionSwitch(m.sessionID)
+ this.prBridge.poller.setActiveWorktreeId(this.state?.getSession(m.sessionID)?.worktreeId ?? undefined)
}
// After clearSession, clear active tracking and re-register worktree sessions
if (m.type === "clearSession") {
this.activeSessionId = undefined
+ this.connectionService.unregisterFocused("agent-manager")
void Promise.resolve().then(() => {
if (!this.panel || !this.state) return
for (const id of this.state.worktreeSessionIds()) {
@@ -671,6 +717,7 @@ export class AgentManagerProvider implements Disposable {
}
// Remove from state BEFORE disk removal so pollers immediately stop targeting this worktree.
this.statsPoller.skipWorktree(worktreeId)
+ this.prBridge.remove(worktreeId)
const orphaned = state.removeWorktree(worktreeId)
if (shouldStopDiffPolling(worktree.path, orphaned, this.cachedDiffTarget, this.diffSessionId)) {
this.stopDiffPolling()
@@ -679,11 +726,11 @@ export class AgentManagerProvider implements Disposable {
this.pushState()
// Disk removal after state is clean — pollers no longer reference this worktree.
try {
- await manager.removeWorktree(worktree.path, worktree.branch)
+ await manager.removeWorktree(worktree.path, worktree.originalBranch ?? worktree.branch)
} catch (error) {
this.log(`Failed to remove worktree from disk: ${error}`)
}
- this.log(`Deleted worktree ${worktreeId} (${worktree.branch})`)
+ this.log(`Deleted worktree ${worktreeId} (${worktree.originalBranch ?? worktree.branch})`)
return null
}
@@ -889,9 +936,9 @@ export class AgentManagerProvider implements Disposable {
continue
}
- await this.runSetupScriptForWorktree(wt.result.path, wt.result.branch)
+ await this.runSetupScriptForWorktree(wt.result.path, wt.result.branch, wt.worktree.id)
- const session = await this.createSessionInWorktree(wt.result.path, wt.result.branch)
+ const session = await this.createSessionInWorktree(wt.result.path, wt.result.branch, wt.worktree.id)
if (!session) {
const state = this.getStateManager()
const manager = this.getWorktreeManager()
@@ -904,7 +951,7 @@ export class AgentManagerProvider implements Disposable {
const state = this.getStateManager()!
state.addSession(session.id, wt.worktree.id)
this.registerWorktreeSession(session.id, wt.result.path)
- this.notifyWorktreeReady(session.id, wt.result)
+ this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id)
// Set the per-version model immediately so the UI selector reflects
// the correct model as soon as the worktree appears, before Phase 2.
@@ -1382,6 +1429,7 @@ export class AgentManagerProvider implements Disposable {
status: "error",
message: `Setup script failed: ${msg}`,
branch,
+ worktreeId,
})
}
}
@@ -1410,6 +1458,10 @@ export class AgentManagerProvider implements Disposable {
if (!this.panel) return
this.panel.sessions.setSessionDirectory(sessionId, directory)
this.panel.sessions.trackSession(sessionId)
+ // Recover any permission/question prompts that arrived before the session
+ // was tracked. The CLI backend may have emitted permission.asked between
+ // session.create() returning and this registration completing.
+ this.panel.sessions.recoverPendingPrompts()
}
private onWorktreePresence(result: WorktreePresenceResult): void {
@@ -1428,12 +1480,20 @@ export class AgentManagerProvider implements Disposable {
const entries = result.worktrees.filter((item) => ids.has(item.worktreeId))
if (entries.length === 0) return
+ // Sync branches from git worktree list (no extra git calls)
+ let branchChanged = false
+ for (const entry of entries) {
+ if (entry.branch && state.updateWorktreeBranch(entry.worktreeId, entry.branch)) {
+ branchChanged = true
+ }
+ }
+
const next = new Set(entries.filter((entry) => entry.missing).map((entry) => entry.worktreeId))
- const changed =
+ const staleChanged =
next.size !== this.staleWorktreeIds.size || [...next].some((worktreeId) => !this.staleWorktreeIds.has(worktreeId))
this.staleWorktreeIds = next
- if (changed) {
+ if (staleChanged || branchChanged) {
this.pushState()
}
}
@@ -1464,6 +1524,7 @@ export class AgentManagerProvider implements Disposable {
type: "agentManager.state",
worktrees,
sessions: state.getSessions(),
+ sections: state.getSections(),
staleWorktreeIds,
tabOrder: state.getTabOrder(),
worktreeOrder: state.getWorktreeOrder(),
@@ -1474,6 +1535,7 @@ export class AgentManagerProvider implements Disposable {
})
this.statsPoller.setEnabled(worktrees.length > 0 || this.panel !== undefined)
+ this.prBridge.poller.setEnabled(worktrees.length > 0)
}
/** Push empty state when the folder is not a git repo or has no folder open. */
@@ -1613,6 +1675,66 @@ export class AgentManagerProvider implements Disposable {
}
}
+ /** Revert a single file in a worktree back to the merge-base state. */
+ private async onRevertWorktreeFile(sessionId: string, file: string): Promise {
+ if (!file) return
+ if (this.stateReady) {
+ await this.stateReady.catch((err) => this.log("stateReady rejected, continuing revert resolve:", err))
+ }
+
+ const target =
+ this.cachedDiffTarget?.sessionId === sessionId ? this.cachedDiffTarget : await this.resolveDiffTarget(sessionId)
+ if (!target) {
+ this.postToWebview({
+ type: "agentManager.revertWorktreeFileResult",
+ sessionId,
+ file,
+ status: "error",
+ message: "Could not resolve diff target",
+ })
+ return
+ }
+
+ // Look up the file status from the cached diffs so we know if it's added/modified/deleted
+ let status: "added" | "deleted" | "modified" | undefined
+ try {
+ const client = this.connectionService.getClient()
+ const { data } = await client.worktree.diffFile(
+ { directory: target.directory, base: target.baseBranch, file },
+ { throwOnError: true },
+ )
+ status = data?.status
+ } catch (err) {
+ this.log("Failed to look up file status for revert:", err)
+ }
+
+ try {
+ const result = await this.gitOps.revertFile(target.directory, target.baseBranch, file, status)
+ this.postToWebview({
+ type: "agentManager.revertWorktreeFileResult",
+ sessionId,
+ file,
+ status: result.ok ? "success" : "error",
+ message: result.message,
+ })
+
+ // After successful revert, trigger a diff refresh so the UI updates
+ if (result.ok) {
+ void this.onRequestWorktreeDiff(sessionId)
+ }
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err)
+ this.log("Failed to revert worktree file:", message)
+ this.postToWebview({
+ type: "agentManager.revertWorktreeFileResult",
+ sessionId,
+ file,
+ status: "error",
+ message,
+ })
+ }
+ }
+
// ---------------------------------------------------------------------------
// Diff polling
// ---------------------------------------------------------------------------
@@ -1886,13 +2008,21 @@ export class AgentManagerProvider implements Disposable {
)
}
+ private handleSection(m: AgentManagerInMessage): boolean {
+ return handleSection(this.state, m, () => this.pushState())
+ }
+
public postMessage(message: unknown): void {
this.panel?.postMessage(message)
}
public dispose(): void {
+ this.connectionService.unregisterFocused("agent-manager")
+ this.connectionService.registerOpen("agent-manager", [])
this.stopDiffPolling()
this.statsPoller.stop()
+ this.gitOps.dispose()
+ this.prBridge.poller.stop()
this.terminalManager.dispose()
this.panel?.dispose()
this.outputChannel.dispose()
diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts
index 9aa393513c..3f36edb587 100644
--- a/packages/kilo-vscode/src/agent-manager/GitOps.ts
+++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts
@@ -4,11 +4,14 @@ import * as fs from "fs/promises"
import { spawn } from "../util/process"
import simpleGit from "simple-git"
import { parseWorktreeList, normalizePath } from "./git-import"
+import type { Semaphore } from "./semaphore"
interface GitOpsOptions {
log: (...args: unknown[]) => void
/** Override git command execution for testing. */
runGit?: (args: string[], cwd: string) => Promise
+ /** Shared concurrency gate for child process spawning. */
+ semaphore?: Semaphore
}
export interface ApplyConflict {
@@ -62,19 +65,49 @@ export function nonInteractiveEnv(): NodeJS.ProcessEnv {
export class GitOps {
private readonly log: (...args: unknown[]) => void
private readonly runGit: (args: string[], cwd: string) => Promise
+ private readonly controller = new AbortController()
+ private readonly semaphore: Semaphore | undefined
+
+ get disposed(): boolean {
+ return this.controller.signal.aborted
+ }
constructor(options: GitOpsOptions) {
this.log = options.log
+ this.semaphore = options.semaphore
this.runGit =
options.runGit ??
((args, cwd) =>
- simpleGit(cwd)
+ simpleGit(cwd, { abort: this.controller.signal })
.raw(args)
.then((out) => out.trim()))
}
+ dispose(): void {
+ if (!this.controller.signal.aborted) {
+ this.controller.abort()
+ }
+ }
+
private raw(args: string[], cwd: string): Promise {
- return this.runGit(args, cwd)
+ const signal = this.controller.signal
+ if (signal.aborted) return Promise.reject(new Error("GitOps disposed"))
+ const invoke = () =>
+ new Promise((resolve, reject) => {
+ const onAbort = () => reject(new Error("GitOps disposed"))
+ signal.addEventListener("abort", onAbort, { once: true })
+ this.runGit(args, cwd).then(
+ (value) => {
+ signal.removeEventListener("abort", onAbort)
+ resolve(value)
+ },
+ (err) => {
+ signal.removeEventListener("abort", onAbort)
+ reject(err)
+ },
+ )
+ })
+ return this.semaphore ? this.semaphore.run(invoke) : invoke()
}
/** Return the name of the currently checked-out branch, or `"HEAD"` if detached. */
@@ -130,14 +163,14 @@ export class GitOps {
}
/** Return the set of worktree paths for the repo, excluding bare entries. */
- async listWorktreePaths(cwd: string): Promise> {
+ async listWorktreePaths(cwd: string): Promise