diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8b64bf4ed5d..9fc57f520d9 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/bun.lock b/bun.lock
index 902e539457e..a3b5041e187 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,6 @@
{
"lockfileVersion": 1,
- "configVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "@kilocode/kilo",
diff --git a/packages/kilo-docs/components/ThemeToggle.tsx b/packages/kilo-docs/components/ThemeToggle.tsx
index 24a21c9cc9b..e73ae559556 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 06795fcbe6a..e1d7b8f039d 100644
--- a/packages/kilo-docs/lib/nav/code-with-ai.ts
+++ b/packages/kilo-docs/lib/nav/code-with-ai.ts
@@ -49,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 ce3a63b9b28..f61bd750bfa 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/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md
index 69ec4bdc5f0..a7d3fe5aab6 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 ce24af58ff3..00000000000
--- 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 2e229d00fdc..9ec66e7591a 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.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
index 0c206fffb56..ed6ff494c9c 100644
--- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
+++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md
@@ -121,10 +121,11 @@ For detailed help on every command and subcommand, see the [CLI Command Referenc
#### 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
@@ -442,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:
@@ -464,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 fc511e0e225..4e0f2a18bb3 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/getting-started/using-kilo-for-free.md b/packages/kilo-docs/pages/getting-started/using-kilo-for-free.md
index 4f6a70d9f63..00a189c0ebd 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/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js
index c6d3804f430..f20702d05fb 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-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx
index a96af21fe11..66c8176cbc3 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.tsx b/packages/kilo-ui/src/components/message-part.tsx
index 663945716bb..41388c8622a 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}
@@ -1254,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].
@@ -1283,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)
}
@@ -1298,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
}
})
@@ -1312,7 +1347,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
return (
-
+
@@ -1321,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 6ea1f5d8ad0..81fa4874be0 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 ca56f4c8fad..db854802662 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 6c452631f04..b9e90c02081 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 69b79df8544..549e60910ed 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 5b01bdb44a9..fe2a70ddb5c 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", 3300],
- },
+ 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 806593bc571..3e718495627 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 d8c72aec7c8..8fcd0ce742a 100644
--- a/packages/kilo-vscode/package.json
+++ b/packages/kilo-vscode/package.json
@@ -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/DiffVirtualProvider.ts b/packages/kilo-vscode/src/DiffVirtualProvider.ts
new file mode 100644
index 00000000000..b585ee71fdd
--- /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 a337470209b..fa9a9b812da 100644
--- a/packages/kilo-vscode/src/KiloProvider.ts
+++ b/packages/kilo-vscode/src/KiloProvider.ts
@@ -36,11 +36,13 @@ import { GitOps } from "./agent-manager/GitOps"
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
import { getWorkspaceRoot } from "./review-utils"
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 {
@@ -112,6 +114,7 @@ const mapAgent = (a: Agent) => ({
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
@@ -145,6 +148,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
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. */
@@ -173,6 +178,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
@@ -197,6 +204,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,
@@ -209,12 +220,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",
@@ -318,6 +346,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
@@ -348,20 +378,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()
}
@@ -380,9 +406,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()
}
@@ -438,6 +466,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 })
}
@@ -493,6 +545,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": {
@@ -576,6 +629,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
@@ -627,6 +681,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) => {
@@ -806,6 +865,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
@@ -1037,6 +1106,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.
@@ -1079,8 +1150,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({
@@ -1155,6 +1225,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.syncWebviewState("initializeConnection")
await this.flushPendingSessionRefresh("initializeConnection")
+ this.recoverPendingPrompts()
// Fetch providers, agents, skills, config, notifications, and session statuses in parallel
await Promise.all([
@@ -1233,6 +1304,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) {
@@ -1318,9 +1390,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
@@ -1377,11 +1448,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)
@@ -2811,6 +2879,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.
@@ -2888,9 +2961,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)
@@ -3222,6 +3296,8 @@ 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?.()
@@ -3233,7 +3309,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 922754e319f..493a6fd519b 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 bebd61406a2..60c2383dbab 100644
--- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
+++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
@@ -25,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"
@@ -76,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
@@ -105,6 +108,7 @@ export class AgentManagerProvider implements Disposable {
hasPersistedPR: (id: string) => !!this.state?.getWorktree(id)?.prNumber,
openExternal: (u) => this.host.openExternal(u),
log: (...a) => this.log(...a),
+ semaphore,
})
}
@@ -197,24 +201,25 @@ 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()
}
// ---------------------------------------------------------------------------
@@ -232,8 +237,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") {
@@ -245,6 +254,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
}
@@ -392,10 +410,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
@@ -423,6 +449,7 @@ 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)
}
@@ -430,6 +457,7 @@ export class AgentManagerProvider implements Disposable {
// 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()) {
@@ -907,9 +935,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()
@@ -922,7 +950,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.
@@ -1400,6 +1428,7 @@ export class AgentManagerProvider implements Disposable {
status: "error",
message: `Setup script failed: ${msg}`,
branch,
+ worktreeId,
})
}
}
@@ -1428,6 +1457,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 {
@@ -1641,6 +1674,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
// ---------------------------------------------------------------------------
@@ -1923,6 +2016,8 @@ export class AgentManagerProvider implements Disposable {
}
public dispose(): void {
+ this.connectionService.unregisterFocused("agent-manager")
+ this.connectionService.registerOpen("agent-manager", [])
this.stopDiffPolling()
this.statsPoller.stop()
this.gitOps.dispose()
diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts
index 6c43f64d1e4..3f36edb587e 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 {
@@ -63,6 +66,7 @@ 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
@@ -70,6 +74,7 @@ export class GitOps {
constructor(options: GitOpsOptions) {
this.log = options.log
+ this.semaphore = options.semaphore
this.runGit =
options.runGit ??
((args, cwd) =>
@@ -87,20 +92,22 @@ export class GitOps {
private raw(args: string[], cwd: string): Promise {
const signal = this.controller.signal
if (signal.aborted) return Promise.reject(new Error("GitOps disposed"))
- return 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)
- },
- )
- })
+ 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. */
@@ -286,6 +293,55 @@ export class GitOps {
}
}
+ /**
+ * Revert a single file in a worktree back to the merge-base state.
+ * For modified/deleted files: restores the file from the merge-base commit.
+ * For added (new) files: removes the file from the worktree.
+ */
+ async revertFile(
+ cwd: string,
+ baseBranch: string,
+ file: string,
+ status?: "added" | "deleted" | "modified",
+ ): Promise<{ ok: boolean; message: string }> {
+ // Validate path: no absolute paths, no ".." traversal
+ if (nodePath.isAbsolute(file) || file.split(/[\\/]/).includes("..")) {
+ return { ok: false, message: "Invalid file path" }
+ }
+
+ const base = (await this.raw(["merge-base", "HEAD", baseBranch], cwd).catch(() => "")).trim()
+ if (!base) {
+ return { ok: false, message: "Could not resolve merge-base" }
+ }
+
+ if (status === "added") {
+ // New file — remove it from disk and unstage
+ const full = nodePath.resolve(cwd, file)
+ const root = await fs.realpath(cwd)
+ const resolved = await fs.realpath(full).catch(() => full)
+ if (resolved !== root && !resolved.startsWith(root + nodePath.sep)) {
+ return { ok: false, message: "File path outside worktree" }
+ }
+ await fs.rm(full, { force: true })
+ // Also remove from git index in case it was staged
+ await this.raw(["rm", "--cached", "--force", "--ignore-unmatch", "--", file], cwd).catch(() => "")
+ return { ok: true, message: "Removed added file" }
+ }
+
+ // Modified or deleted file — restore from merge-base
+ const result = await this.exec(["checkout", base, "--", file], cwd)
+ if (result.code !== 0) {
+ return { ok: false, message: result.stderr.trim() || "Failed to revert file" }
+ }
+ // Only unstage for modified files. For deleted files the checkout already
+ // restored the file into the index correctly — resetting to HEAD would drop
+ // it from the index and make it appear as a new untracked file.
+ if (status === "modified") {
+ await this.raw(["reset", "HEAD", "--", file], cwd).catch(() => "")
+ }
+ return { ok: true, message: "Reverted file to base" }
+ }
+
async checkApplyPatch(targetPath: string, patch: string): Promise {
if (!patch.trim()) {
return { ok: true, conflicts: [], message: "No changes to apply" }
@@ -364,37 +420,39 @@ export class GitOps {
if (this.controller.signal.aborted) {
return Promise.resolve({ code: 1, stdout: "", stderr: "GitOps disposed" })
}
- return new Promise((resolve) => {
- const child = spawn("git", args, {
- cwd,
- env: options?.env,
- signal: this.controller.signal,
- stdio: ["pipe", "pipe", "pipe"],
- })
+ const invoke = () =>
+ new Promise((resolve) => {
+ const child = spawn("git", args, {
+ cwd,
+ env: options?.env,
+ signal: this.controller.signal,
+ stdio: ["pipe", "pipe", "pipe"],
+ })
- if (options?.stdin !== undefined) {
- if (!child.stdin) {
- resolve({ code: 1, stdout: "", stderr: "stdin not available for git process" })
- return
+ if (options?.stdin !== undefined) {
+ if (!child.stdin) {
+ resolve({ code: 1, stdout: "", stderr: "stdin not available for git process" })
+ return
+ }
+ child.stdin.end(options.stdin)
}
- child.stdin.end(options.stdin)
- }
- const out: Buffer[] = []
- const err: Buffer[] = []
- child.stdout?.on("data", (chunk: Buffer) => out.push(chunk))
- child.stderr?.on("data", (chunk: Buffer) => err.push(chunk))
+ const out: Buffer[] = []
+ const err: Buffer[] = []
+ child.stdout?.on("data", (chunk: Buffer) => out.push(chunk))
+ child.stderr?.on("data", (chunk: Buffer) => err.push(chunk))
- child.on("error", (error) => {
- resolve({ code: 1, stdout: "", stderr: error.message })
- })
- child.on("close", (code) => {
- resolve({
- code: code ?? 1,
- stdout: Buffer.concat(out).toString("utf8"),
- stderr: Buffer.concat(err).toString("utf8"),
+ child.on("error", (error) => {
+ resolve({ code: 1, stdout: "", stderr: error.message })
+ })
+ child.on("close", (code) => {
+ resolve({
+ code: code ?? 1,
+ stdout: Buffer.concat(out).toString("utf8"),
+ stderr: Buffer.concat(err).toString("utf8"),
+ })
})
})
- })
+ return this.semaphore ? this.semaphore.run(invoke) : invoke()
}
}
diff --git a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts
index 301261d137e..2cde458d432 100644
--- a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts
+++ b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts
@@ -3,6 +3,7 @@ import * as path from "path"
import type { KiloClient, FileDiff } from "@kilocode/sdk/v2/client"
import { remoteRef, type Worktree } from "./WorktreeStateManager"
import type { GitOps } from "./GitOps"
+import type { Semaphore } from "./semaphore"
import { normalizePath } from "./git-import"
export interface WorktreeStats {
@@ -45,6 +46,8 @@ interface GitStatsPollerOptions {
onWorktreePresence?: (result: WorktreePresenceResult) => void
log: (...args: unknown[]) => void
intervalMs?: number
+ /** Shared concurrency gate for child process spawning. */
+ semaphore?: Semaphore
}
export class GitStatsPoller {
@@ -154,15 +157,20 @@ export class GitStatsPoller {
return
}
+ // Gate the HTTP diffSummary call through the semaphore but NOT the
+ // aheadBehind call — that goes through GitOps.raw() which already
+ // acquires the same semaphore. Wrapping both would deadlock.
+ const gate = this.options.semaphore
+ const diff = (dir: string, base: string) => {
+ const invoke = () => client.worktree.diffSummary({ directory: dir, base }, { throwOnError: true })
+ return gate ? gate.run(invoke) : invoke()
+ }
const stats = (
await Promise.all(
active.map(async (wt) => {
try {
const base = remoteRef(wt)
- const [{ data: diffs }, ab] = await Promise.all([
- client.worktree.diffSummary({ directory: wt.path, base }, { throwOnError: true }),
- this.git.aheadBehind(wt.path, base),
- ])
+ const [{ data: diffs }, ab] = await Promise.all([diff(wt.path, base), this.git.aheadBehind(wt.path, base)])
const files = diffs.length
const additions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.additions, 0)
const deletions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.deletions, 0)
@@ -260,8 +268,10 @@ export class GitStatsPoller {
try {
if (base && client) {
this.options.log(`Local stats: using HTTP client with base=${base}`)
+ const gate = this.options.semaphore
+ const invoke = () => client.worktree.diffSummary({ directory: root, base }, { throwOnError: true })
const [{ data: diffs }, ab] = await Promise.all([
- client.worktree.diffSummary({ directory: root, base }, { throwOnError: true }),
+ gate ? gate.run(invoke) : invoke(),
this.git.aheadBehind(root, base),
])
files = diffs.length
diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts
index 1b91b952904..f655616b96e 100644
--- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts
+++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts
@@ -1,7 +1,9 @@
+import type { ExecFileOptionsWithStringEncoding } from "child_process"
import type { Worktree } from "./WorktreeStateManager"
import type { PRStatus, PRCheck, PRComment, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types"
import { execWithShellEnv } from "./shell-env"
import { classifyPRError } from "./git-import"
+import type { Semaphore } from "./semaphore"
interface PRStatusPollerOptions {
getWorktrees: () => Worktree[]
@@ -9,12 +11,16 @@ interface PRStatusPollerOptions {
onStatus: (worktreeId: string, pr: PRStatus | null, error?: "gh_missing" | "gh_auth" | "fetch_failed") => void
log: (...args: unknown[]) => void
intervalMs?: number
+ /** Shared concurrency gate for child process spawning. */
+ semaphore?: Semaphore
}
const GH_PROBE_TTL = 300_000 // 5 minutes — gh installation state rarely changes at runtime
const MAX_BACKOFF = 120_000 // 2 minutes — cap for exponential backoff on repeated errors
const BACKOFF_MULTIPLIER = 2
const PR_LOOKUP_TTL = 10_000 // 10 seconds — short TTL; only the active worktree polls so this stays cheap
+const FULL_SYNC_INTERVAL = 120_000 // 2 minutes — periodic sync of ALL worktrees (badges stay fresh)
+const FULL_SYNC_CONCURRENCY = 3 // max parallel gh processes during a full sync (caps the burst)
export class PRStatusPoller {
private timer: ReturnType | undefined
@@ -29,10 +35,23 @@ export class PRStatusPoller {
private activeWorktreeId: string | undefined
private cachedRepo: { owner: string; name: string; cwd: string } | undefined
private prCache = new Map()
+ private lastFullSync = 0 // timestamp of last full (all-worktree) sync
private readonly intervalMs: number
+ private readonly semaphore: Semaphore | undefined
constructor(private readonly options: PRStatusPollerOptions) {
this.intervalMs = options.intervalMs ?? 15_000
+ this.semaphore = options.semaphore
+ }
+
+ /** Run a command through the shared concurrency gate (when configured). */
+ private shell(
+ cmd: string,
+ args: string[],
+ options?: Omit,
+ ): Promise<{ stdout: string; stderr: string }> {
+ const invoke = () => execWithShellEnv(cmd, args, options)
+ return this.semaphore ? this.semaphore.run(invoke) : invoke()
}
setEnabled(enabled: boolean): void {
@@ -56,6 +75,7 @@ export class PRStatusPoller {
this.timer = undefined
this.prCache.clear()
this.lastHash.clear()
+ this.lastFullSync = 0
void this.poll()
return
}
@@ -80,6 +100,7 @@ export class PRStatusPoller {
this.ghProbeTime = 0
this.cachedRepo = undefined
this.prCache.clear()
+ this.lastFullSync = 0
}
/** Force-refresh a specific worktree immediately, bypassing the PR cache. */
@@ -135,7 +156,7 @@ export class PRStatusPoller {
return this.ghAvailable
}
try {
- await execWithShellEnv("gh", ["--version"], { timeout: 5_000 })
+ await this.shell("gh", ["--version"], { timeout: 5_000 })
this.ghAvailable = true
} catch {
this.ghAvailable = false
@@ -159,19 +180,26 @@ export class PRStatusPoller {
this.lastError = undefined
- // Only poll the active worktree on each timer tick. Inactive worktrees
- // refresh when selected (setActiveWorktreeId) or manually (refresh()).
- // On first poll (lastHash empty) fetch all worktrees once to populate badges.
+ // Most ticks only poll the active worktree for fast feedback. Every
+ // FULL_SYNC_INTERVAL we poll ALL worktrees so badges stay current even
+ // for sessions that aren't selected (e.g. CI results changing).
+ // The very first poll (lastHash empty) also fetches everything.
const worktrees = this.options.getWorktrees()
+ const now = Date.now()
const initial = this.lastHash.size === 0
- const targets = initial ? worktrees : worktrees.filter((wt) => wt.id === this.activeWorktreeId)
+ const full = initial || now - this.lastFullSync >= FULL_SYNC_INTERVAL
+ const targets = full ? worktrees : worktrees.filter((wt) => wt.id === this.activeWorktreeId)
+ if (full) this.lastFullSync = now
if (targets.length === 0) {
this.failures = 0
return
}
- const results = await Promise.allSettled(targets.map((wt) => this.fetchOne(wt.id)))
+ const thunks = targets.map((wt) => () => this.fetchOne(wt.id))
+ const results = full
+ ? await settled(thunks, FULL_SYNC_CONCURRENCY)
+ : await Promise.allSettled(thunks.map((fn) => fn()))
const ok = results.every((r) => r.status === "fulfilled")
if (ok) {
this.failures = 0
@@ -267,7 +295,7 @@ export class PRStatusPoller {
if (branch) args.push(branch)
args.push("--json", PRStatusPoller.PR_JSON_FIELDS)
- const { stdout } = await execWithShellEnv("gh", args, { cwd, timeout: 15_000 })
+ const { stdout } = await this.shell("gh", args, { cwd, timeout: 15_000 })
return parsePRResult(stdout)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
@@ -279,11 +307,11 @@ export class PRStatusPoller {
/** Search for PRs containing the current HEAD SHA. Finds PRs when branch name/tracking ref don't match. */
private async ghPRListBySHA(cwd: string): Promise {
try {
- const { stdout: sha } = await execWithShellEnv("git", ["rev-parse", "HEAD"], { cwd, timeout: 5_000 })
+ const { stdout: sha } = await this.shell("git", ["rev-parse", "HEAD"], { cwd, timeout: 5_000 })
const head = sha.trim()
if (!head) return null
- const { stdout } = await execWithShellEnv(
+ const { stdout } = await this.shell(
"gh",
[
"pr",
@@ -325,7 +353,7 @@ export class PRStatusPoller {
items: PRCheck[]
}> {
try {
- const { stdout } = await execWithShellEnv(
+ const { stdout } = await this.shell(
"gh",
["pr", "checks", String(prNumber), "--json", "name,state,link,startedAt,completedAt"],
{ cwd, timeout: 15_000 },
@@ -363,7 +391,7 @@ export class PRStatusPoller {
if (this.cachedRepo && this.cachedRepo.cwd === cwd) {
return this.cachedRepo
}
- const { stdout } = await execWithShellEnv("gh", ["repo", "view", "--json", "owner,name"], {
+ const { stdout } = await this.shell("gh", ["repo", "view", "--json", "owner,name"], {
cwd,
timeout: 10_000,
})
@@ -403,7 +431,7 @@ export class PRStatusPoller {
}
}`
- const { stdout } = await execWithShellEnv(
+ const { stdout } = await this.shell(
"gh",
[
"api",
@@ -519,3 +547,22 @@ function formatCheckDuration(startedAt?: string, completedAt?: string): string |
const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000)
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`
}
+
+/** Run async thunks with bounded concurrency, returning settled results. */
+async function settled(thunks: (() => Promise)[], concurrency: number): Promise[]> {
+ const results: PromiseSettledResult[] = new Array(thunks.length)
+ let idx = 0
+ async function run(): Promise {
+ while (idx < thunks.length) {
+ const i = idx++
+ const fn = thunks[i]!
+ try {
+ results[i] = { status: "fulfilled", value: await fn() }
+ } catch (reason) {
+ results[i] = { status: "rejected", reason }
+ }
+ }
+ }
+ await Promise.all(Array.from({ length: Math.min(concurrency, thunks.length) }, () => run()))
+ return results
+}
diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts
index 932e35e5604..5d6c92edf9a 100644
--- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts
+++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts
@@ -858,6 +858,15 @@ export class WorktreeManager {
this.log(`defaultBranch: branchLocal failed: ${e}`)
}
+ // Check if this is an empty repo with no commits (unborn branch).
+ // rev-parse --verify HEAD exits non-zero only when HEAD has no target
+ // commit, which is the definitive test for an unborn branch.
+ try {
+ await this.git.raw(["rev-parse", "--verify", "HEAD"])
+ } catch {
+ throw new Error("This repository has no commits yet. Create an initial commit before using worktrees.")
+ }
+
throw new Error("Could not determine default branch")
}
diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts
index 3765785d96f..3b051b3680d 100644
--- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts
+++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts
@@ -247,12 +247,12 @@ export class WorktreeStateManager {
return session
}
- /** Move an existing session to a worktree (promotion). */
- moveSession(sessionId: string, worktreeId: string): void {
+ /** Move an existing session to a worktree (or back to local when null). */
+ moveSession(sessionId: string, worktreeId: string | null): void {
const session = this.sessions.get(sessionId)
if (!session) return
session.worktreeId = worktreeId
- this.log(`Moved session ${sessionId} to worktree ${worktreeId}`)
+ this.log(`Moved session ${sessionId} to ${worktreeId ?? "local"}`)
void this.save()
}
@@ -304,6 +304,11 @@ export class WorktreeStateManager {
if (!wt.sectionId) top.add(wt.id)
}
this.worktreeOrder = order.filter((id) => top.has(id))
+ // Append any sections/ungrouped worktrees missing from the incoming order
+ const present = new Set(this.worktreeOrder)
+ for (const id of top) {
+ if (!present.has(id)) this.worktreeOrder.push(id)
+ }
void this.save()
}
@@ -380,6 +385,11 @@ export class WorktreeStateManager {
}
moveSection(id: string, dir: -1 | 1): void {
+ // Ensure the section is in worktreeOrder (it may be missing if drag-and-drop
+ // overwrote the order before this section was tracked)
+ if (this.sections.has(id) && !this.worktreeOrder.includes(id)) {
+ this.worktreeOrder.push(id)
+ }
const top = this.worktreeOrder.filter((item) => {
if (this.sections.has(item)) return true
const wt = this.worktrees.get(item)
diff --git a/packages/kilo-vscode/src/agent-manager/git-import.ts b/packages/kilo-vscode/src/agent-manager/git-import.ts
index 342a0448efd..ae3f2c37a73 100644
--- a/packages/kilo-vscode/src/agent-manager/git-import.ts
+++ b/packages/kilo-vscode/src/agent-manager/git-import.ts
@@ -29,7 +29,7 @@ interface WorktreeEntry {
type PRErrorKind = "not_found" | "gh_missing" | "gh_auth" | "unknown"
-export type WorktreeSetupErrorCode = "git_not_found" | "not_git_repo" | "lfs_missing"
+export type WorktreeSetupErrorCode = "git_not_found" | "not_git_repo" | "lfs_missing" | "no_commits"
export function parsePRUrl(url: string): PRUrlParts | null {
let normalized = url.trim()
@@ -158,5 +158,6 @@ export function classifyWorktreeError(msg: string): WorktreeSetupErrorCode | und
if (msg.includes("ENOENT") || msg.includes("not found in PATH")) return "git_not_found"
if (msg.includes("not a git repository")) return "not_git_repo"
if (msg.includes("Git LFS") && msg.includes("not found")) return "lfs_missing"
+ if (msg.includes("no commits yet")) return "no_commits"
return undefined
}
diff --git a/packages/kilo-vscode/src/agent-manager/host.ts b/packages/kilo-vscode/src/agent-manager/host.ts
index 596e32cf88c..6dc5e658280 100644
--- a/packages/kilo-vscode/src/agent-manager/host.ts
+++ b/packages/kilo-vscode/src/agent-manager/host.ts
@@ -38,6 +38,8 @@ export interface SessionProvider {
trackSession(id: string): void
refreshSessions(): void
registerSession(session: Session): void
+ /** Recover any pending permission/question prompts for tracked sessions. */
+ recoverPendingPrompts(): void
dispose(): void
}
diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
index 15d5d754d7e..03146dc49a6 100644
--- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
+++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
@@ -7,6 +7,7 @@
import type { Worktree } from "./WorktreeStateManager"
import type { AgentManagerOutMessage, PRStatus } from "./types"
import type { Disposable } from "./host"
+import type { Semaphore } from "./semaphore"
import { PRStatusPoller } from "./PRStatusPoller"
interface PRBridgeHost {
@@ -17,6 +18,7 @@ interface PRBridgeHost {
hasPersistedPR(id: string): boolean
openExternal(url: string): void
log(...args: unknown[]): void
+ semaphore?: Semaphore
}
/** Minimal panel surface needed by the bridge (subset of PanelContext). */
@@ -43,6 +45,7 @@ export class PRStatusBridge {
hasPersistedPR: (id: string) => boolean
openExternal: (url: string) => void
log: (...args: unknown[]) => void
+ semaphore?: Semaphore
}): PRStatusBridge {
return new PRStatusBridge(opts)
}
@@ -85,6 +88,7 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) {
return {
getWorktrees: () => host.getWorktrees(),
getWorkspaceRoot: () => host.getWorkspaceRoot(),
+ semaphore: host.semaphore,
onStatus: (id: string, pr: PRStatus | null, err?: "gh_missing" | "gh_auth" | "fetch_failed") => {
if (err) {
// Don't forward errors to the webview when we have prior PR data
diff --git a/packages/kilo-vscode/src/agent-manager/semaphore.ts b/packages/kilo-vscode/src/agent-manager/semaphore.ts
new file mode 100644
index 00000000000..37f3ab9cdd2
--- /dev/null
+++ b/packages/kilo-vscode/src/agent-manager/semaphore.ts
@@ -0,0 +1,41 @@
+/**
+ * Bounded-concurrency gate for git/gh child processes.
+ *
+ * Shared across GitOps and PRStatusPoller so that all polling loops
+ * (GitStatsPoller, PRStatusPoller, diff watcher) compete for the same
+ * slots. Prevents process storms when many worktrees are active.
+ */
+export class Semaphore {
+ private running = 0
+ private readonly pending: (() => void)[] = []
+
+ constructor(private readonly limit: number) {}
+
+ async run(fn: () => Promise): Promise {
+ await this.acquire()
+ try {
+ return await fn()
+ } finally {
+ this.release()
+ }
+ }
+
+ private acquire(): Promise {
+ if (this.running < this.limit) {
+ this.running++
+ return Promise.resolve()
+ }
+ return new Promise((resolve) => {
+ this.pending.push(() => {
+ this.running++
+ resolve()
+ })
+ })
+ }
+
+ private release(): void {
+ this.running--
+ const next = this.pending.shift()
+ if (next) next()
+ }
+}
diff --git a/packages/kilo-vscode/src/agent-manager/task-runner.ts b/packages/kilo-vscode/src/agent-manager/task-runner.ts
index 0c9e7fa7c46..1329939e274 100644
--- a/packages/kilo-vscode/src/agent-manager/task-runner.ts
+++ b/packages/kilo-vscode/src/agent-manager/task-runner.ts
@@ -28,7 +28,16 @@ export async function executeVscodeTask(config: SetupTaskConfig): Promise {
let done = false
diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts
index 264f1f46107..2dc76cc6a01 100644
--- a/packages/kilo-vscode/src/agent-manager/types.ts
+++ b/packages/kilo-vscode/src/agent-manager/types.ts
@@ -228,6 +228,14 @@ interface WorktreeDiffFileMessage {
diff: WorktreeDiffEntry | null
}
+interface RevertWorktreeFileResultMessage {
+ type: "agentManager.revertWorktreeFileResult"
+ sessionId: string
+ file: string
+ status: "success" | "error"
+ message: string
+}
+
interface PRStatusOutMessage {
type: "agentManager.prStatus"
worktreeId: string
@@ -262,6 +270,7 @@ export type AgentManagerOutMessage =
| WorktreeDiffLoadingMessage
| WorktreeDiffMessage
| WorktreeDiffFileMessage
+ | RevertWorktreeFileResultMessage
| PRStatusOutMessage
| ActionOutMessage
@@ -305,6 +314,18 @@ interface CloseSessionIn {
sessionId: string
}
+/** Persist a non-worktree session to agent-manager.json (worktreeId = null). */
+interface PersistSessionIn {
+ type: "agentManager.persistSession"
+ sessionId: string
+}
+
+/** Remove a non-worktree session from agent-manager.json. */
+interface ForgetSessionIn {
+ type: "agentManager.forgetSession"
+ sessionId: string
+}
+
interface ConfigureSetupScriptIn {
type: "agentManager.configureSetupScript"
}
@@ -440,6 +461,12 @@ interface StopDiffWatchIn {
type: "agentManager.stopDiffWatch"
}
+interface RevertWorktreeFileIn {
+ type: "agentManager.revertWorktreeFile"
+ sessionId: string
+ file: string
+}
+
interface RefreshPRIn {
type: "agentManager.refreshPR"
worktreeId: string
@@ -450,6 +477,11 @@ interface OpenPRIn {
worktreeId: string
}
+interface OpenSessionsIn {
+ type: "agentManager.openSessions"
+ sessionIDs: string[]
+}
+
interface OpenFileIn {
type: "agentManager.openFile"
sessionId: string
@@ -574,6 +606,8 @@ export type AgentManagerInMessage =
| OpenLocallyIn
| AddSessionToWorktreeIn
| CloseSessionIn
+ | PersistSessionIn
+ | ForgetSessionIn
| ForkSessionIn
| ConfigureSetupScriptIn
| ShowTerminalIn
@@ -601,8 +635,10 @@ export type AgentManagerInMessage =
| ApplyWorktreeDiffIn
| StartDiffWatchIn
| StopDiffWatchIn
+ | RevertWorktreeFileIn
| RefreshPRIn
| OpenPRIn
+ | OpenSessionsIn
| OpenFileIn
| GenericOpenFileIn
| PreviewImageIn
diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts
index 7ea4403cbc0..9a539ccf0ba 100644
--- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts
+++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts
@@ -9,17 +9,24 @@ import * as vscode from "vscode"
import type { Host, PanelContext, OutputHandle, SessionProvider, Disposable } from "./host"
import type { KiloConnectionService } from "../services/cli-backend"
import { KiloProvider } from "../KiloProvider"
+import { DiffVirtualProvider } from "../DiffVirtualProvider"
import { buildWebviewHtml } from "../utils"
import { openFileInEditor, getWorkspaceRoot } from "../review-utils"
import { TelemetryProxy, type TelemetryEventName } from "../services/telemetry"
export class VscodeHost implements Host {
+ private diffVirtual: DiffVirtualProvider | undefined
+
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
) {}
+ setDiffVirtualProvider(provider: DiffVirtualProvider): void {
+ this.diffVirtual = provider
+ }
+
openPanel(opts: {
onBeforeMessage: (msg: Record) => Promise | null>
}): PanelContext {
@@ -74,6 +81,9 @@ export class VscodeHost implements Host {
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
slimEditMetadata: true,
})
+ if (this.diffVirtual) {
+ provider.setDiffVirtualProvider(this.diffVirtual)
+ }
provider.attachToWebview(panel.webview, {
onBeforeMessage: opts.onBeforeMessage,
})
@@ -85,6 +95,7 @@ export class VscodeHost implements Host {
trackSession: (id) => provider.trackSession(id),
refreshSessions: () => provider.refreshSessions(),
registerSession: (s) => provider.registerSession(s),
+ recoverPendingPrompts: () => provider.recoverPendingPrompts(),
dispose: () => provider.dispose(),
}
diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts
index 438b26c3a9c..60b30ecc877 100644
--- a/packages/kilo-vscode/src/extension.ts
+++ b/packages/kilo-vscode/src/extension.ts
@@ -3,6 +3,7 @@ import { KiloProvider } from "./KiloProvider"
import { AgentManagerProvider } from "./agent-manager/AgentManagerProvider"
import { VscodeHost } from "./agent-manager/vscode-host"
import { DiffViewerProvider } from "./DiffViewerProvider"
+import { DiffVirtualProvider } from "./DiffVirtualProvider"
import { SettingsEditorProvider } from "./SettingsEditorProvider"
import { SubAgentViewerProvider } from "./SubAgentViewerProvider"
import { EXTENSION_DISPLAY_NAME } from "./constants"
@@ -15,6 +16,7 @@ import { TelemetryProxy } from "./services/telemetry"
import { registerCommitMessageService } from "./services/commit-message"
import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions"
import { registerToggleAutoApprove } from "./commands/toggle-auto-approve"
+import { RemoteStatusService } from "./services/RemoteStatusService"
// Activated via "onStartupFinished" (package.json) so that commands, code actions, keybindings,
// autocomplete, commit-message generation, and URI deep links all work immediately — without
@@ -32,8 +34,13 @@ export function activate(context: vscode.ExtensionContext) {
const browserAutomationService = new BrowserAutomationService(connectionService)
browserAutomationService.syncWithSettings()
+ // Create remote status service (one status bar item for all webviews)
+ const remoteService = new RemoteStatusService()
+ context.subscriptions.push(remoteService)
+ connectionService.setRemoteService(remoteService)
+
// Re-register browser automation MCP server on CLI backend reconnect, configure telemetry,
- // and reload autocomplete so it picks up the now-available backend connection.
+ // set remote service client, and reload autocomplete so it picks up the now-available backend connection.
const unsubscribeStateChange = connectionService.onStateChange((state) => {
if (state === "connected") {
browserAutomationService.reregisterIfEnabled()
@@ -41,7 +48,17 @@ export function activate(context: vscode.ExtensionContext) {
if (config) {
telemetry.configure(config.baseUrl, config.password)
}
+ try {
+ remoteService.setClient(connectionService.getClient())
+ console.log("[Kilo New] CLI connected, calling remoteService.refresh()")
+ remoteService.refresh().catch((err) => console.warn("[Kilo New] initial remote refresh failed:", err))
+ } catch {
+ remoteService.setClient(null)
+ }
AutocompleteServiceManager.getInstance()?.load()
+ } else {
+ remoteService.clearState()
+ remoteService.setClient(null)
}
})
@@ -59,6 +76,7 @@ export function activate(context: vscode.ExtensionContext) {
// Create the provider with shared service
const provider = new KiloProvider(context.extensionUri, connectionService, context)
+ provider.setRemoteService(remoteService)
// Register the webview view provider for the sidebar.
// retainContextWhenHidden keeps the webview alive when switching to other sidebar panels.
@@ -102,9 +120,11 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.registerWebviewPanelSerializer("kilo-code.new.TabPanel", {
deserializeWebviewPanel(panel: vscode.WebviewPanel) {
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context)
+ tabProvider.setRemoteService(remoteService)
tabProvider.setContinueInWorktreeHandler((sessionId, progress) =>
agentManagerProvider.continueFromSidebar(sessionId, progress),
)
+ tabProvider.setDiffVirtualProvider(diffVirtualProvider)
tabProvider.resolveWebviewPanel(panel)
tabPanels.set(panel, tabProvider)
panel.onDidDispose(
@@ -128,8 +148,15 @@ export function activate(context: vscode.ExtensionContext) {
})
context.subscriptions.push(diffViewerProvider)
+ // Create diff virtual provider (lightweight single-file diff for permission approval)
+ const diffVirtualProvider = new DiffVirtualProvider(context.extensionUri)
+ provider.setDiffVirtualProvider(diffVirtualProvider)
+ agentManagerHost.setDiffVirtualProvider(diffVirtualProvider)
+ context.subscriptions.push(diffVirtualProvider)
+
// Create settings/profile editor provider (opens in editor area, not sidebar)
const settingsEditorProvider = new SettingsEditorProvider(context.extensionUri, connectionService, context)
+ settingsEditorProvider.setRemoteService(remoteService)
context.subscriptions.push(settingsEditorProvider)
// Create sub-agent viewer provider (read-only editor panel for sub-agent sessions)
@@ -220,8 +247,18 @@ export function activate(context: vscode.ExtensionContext) {
await provider.waitForReady()
provider.postMessage({ type: "triggerTask", text: `Generate a terminal command: ${input}` })
}),
+ vscode.commands.registerCommand("kilo-code.new.toggleRemote", () => {
+ remoteService.toggle().catch((err) => console.error("[Kilo New] toggleRemote command failed:", err))
+ }),
vscode.commands.registerCommand("kilo-code.new.openInTab", () => {
- return openKiloInNewTab(context, connectionService, agentManagerProvider, tabPanels)
+ return openKiloInNewTab(
+ context,
+ connectionService,
+ agentManagerProvider,
+ tabPanels,
+ diffVirtualProvider,
+ remoteService,
+ )
}),
vscode.commands.registerCommand("kilo-code.new.showChanges", () => {
diffViewerProvider.openPanel()
@@ -353,6 +390,8 @@ async function openKiloInNewTab(
connectionService: KiloConnectionService,
agentManagerProvider: AgentManagerProvider,
tabPanels: Map,
+ diffVirtualProvider: DiffVirtualProvider,
+ remoteService: RemoteStatusService,
) {
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((e) => e.viewColumn || 0), 0)
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
@@ -375,9 +414,11 @@ async function openKiloInNewTab(
}
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context)
+ tabProvider.setRemoteService(remoteService)
tabProvider.setContinueInWorktreeHandler((sessionId, progress) =>
agentManagerProvider.continueFromSidebar(sessionId, progress),
)
+ tabProvider.setDiffVirtualProvider(diffVirtualProvider)
tabProvider.resolveWebviewPanel(panel)
tabPanels.set(panel, tabProvider)
diff --git a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts
index 3ec760d7c4a..a7748b5d004 100644
--- a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts
+++ b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts
@@ -112,13 +112,29 @@ function slimMultiedit(state: Record): Record
return next
}
-/** write: strip input.content (entire file). Keep filePath + diagnostics. */
+/** write: strip input.content, raw diff text, and filediff.before/after. Keep filepath + exists + diagnostics. */
function slimWrite(state: Record): Record {
const next = { ...state }
const input = state.input
if (isObj(input) && typeof input.content === "string") {
next.input = { ...input, content: undefined }
}
+ const meta = state.metadata
+ if (isObj(meta)) {
+ const slim: Record = {}
+ if (meta.filepath) slim.filepath = meta.filepath
+ if (meta.exists !== undefined) slim.exists = meta.exists
+ if (meta.diagnostics) slim.diagnostics = meta.diagnostics
+ const fd = meta.filediff
+ if (isObj(fd)) {
+ slim.filediff = {
+ ...(typeof fd.file === "string" ? { file: fd.file } : {}),
+ additions: typeof fd.additions === "number" ? fd.additions : 0,
+ deletions: typeof fd.deletions === "number" ? fd.deletions : 0,
+ }
+ }
+ next.metadata = slim
+ }
return next
}
diff --git a/packages/kilo-vscode/src/kilo-provider/task-session.ts b/packages/kilo-vscode/src/kilo-provider/task-session.ts
new file mode 100644
index 00000000000..cff8f6661d7
--- /dev/null
+++ b/packages/kilo-vscode/src/kilo-provider/task-session.ts
@@ -0,0 +1,17 @@
+type Meta = { sessionId?: string }
+
+type State = {
+ metadata?: Meta
+}
+
+type Part = {
+ type?: string
+ tool?: string
+ metadata?: Meta
+ state?: State
+}
+
+export function childID(part: Part): string | undefined {
+ if (part.type !== "tool" || part.tool !== "task") return undefined
+ return part.metadata?.sessionId ?? part.state?.metadata?.sessionId
+}
diff --git a/packages/kilo-vscode/src/services/RemoteStatusService.ts b/packages/kilo-vscode/src/services/RemoteStatusService.ts
new file mode 100644
index 00000000000..14181005063
--- /dev/null
+++ b/packages/kilo-vscode/src/services/RemoteStatusService.ts
@@ -0,0 +1,129 @@
+import * as vscode from "vscode"
+import type { KiloClient } from "@kilocode/sdk/v2/client"
+import { t } from "./cli-backend/i18n"
+
+export type RemoteState = { enabled: boolean; connected: boolean }
+
+type Listener = (state: RemoteState) => void
+
+/**
+ * Singleton service that owns all remote-control state and the VS Code status bar item.
+ * Replaces the per-webview polling in RemoteIndicator.tsx and ExperimentalTab.tsx
+ * with a push-based model: one status bar item, zero recurring cost for non-remote users.
+ */
+export class RemoteStatusService implements vscode.Disposable {
+ private state: RemoteState = { enabled: false, connected: false }
+ private bar: vscode.StatusBarItem
+ private listeners = new Set()
+ private client: KiloClient | null = null
+
+ constructor() {
+ this.bar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99)
+ this.bar.command = "kilo-code.new.toggleRemote"
+ this.sync()
+ }
+
+ setClient(c: KiloClient | null): void {
+ this.client = c
+ }
+
+ /** Get current state synchronously. */
+ getState(): RemoteState {
+ return this.state
+ }
+
+ updateFromEvent(state: RemoteState): void {
+ this.update(state)
+ }
+
+ /** Subscribe to state changes. Returns an unsubscribe function. */
+ onChange(cb: Listener): () => void {
+ this.listeners.add(cb)
+ return () => this.listeners.delete(cb)
+ }
+
+ clearState(): void {
+ this.update({ enabled: false, connected: false })
+ }
+
+ /** One-shot status fetch — broadcasts via onChange if state changed. */
+ async refresh(): Promise {
+ if (!this.client) return
+ const res = await this.client.remote.status().catch((err: unknown) => {
+ console.warn("[Kilo] remote status refresh failed:", err)
+ return undefined
+ })
+ if (!res?.data) return
+ this.update({ enabled: res.data.enabled, connected: res.data.connected })
+ }
+
+ /** Toggle remote on/off based on current state. */
+ async toggle(): Promise {
+ if (!this.client) return
+ const { data } = await this.client.remote.status(undefined, { throwOnError: true })
+ await this.setEnabled(!data.enabled)
+ }
+
+ /** Enable or disable remote. State updates are pushed via events. */
+ async setEnabled(enabled: boolean): Promise {
+ if (!this.client) return
+ if (enabled) {
+ await this.client.remote.enable(undefined, { throwOnError: true })
+ } else {
+ await this.client.remote.disable(undefined, { throwOnError: true })
+ }
+ this.update({ enabled, connected: false })
+ }
+
+ /**
+ * Handle a remote-related webview message.
+ * Returns a response message to post back to the webview, or null.
+ */
+ async handleMessage(type: string, enabled?: boolean): Promise {
+ switch (type) {
+ case "toggleRemote":
+ await this.toggle()
+ return null
+ case "setRemoteEnabled":
+ if (enabled === undefined) return null
+ await this.setEnabled(enabled)
+ return null
+ case "requestRemoteStatus":
+ void this.refresh()
+ return this.state
+ }
+ return null
+ }
+
+ dispose(): void {
+ this.listeners.clear()
+ this.bar.dispose()
+ }
+
+ // -- internal ---------------------------------------------------------------
+
+ private update(next: RemoteState): void {
+ if (this.state.enabled === next.enabled && this.state.connected === next.connected) return
+ this.state = next
+ this.sync()
+ for (const cb of this.listeners) cb(next)
+ }
+
+ /** Sync status bar appearance to current state. */
+ private sync(): void {
+ if (!this.state.enabled) {
+ this.bar.hide()
+ return
+ }
+ if (this.state.connected) {
+ this.bar.text = "$(radio-tower) Kilo Remote"
+ this.bar.tooltip = t("remote.connected")
+ this.bar.color = new vscode.ThemeColor("testing.iconPassed")
+ } else {
+ this.bar.text = "$(radio-tower) Kilo Remote \u2026"
+ this.bar.tooltip = t("remote.connecting")
+ this.bar.color = new vscode.ThemeColor("editorWarning.foreground")
+ }
+ this.bar.show()
+ }
+}
diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts
index 690f0e1261a..bb4e52f000d 100644
--- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts
+++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts
@@ -1,15 +1,24 @@
import * as vscode from "vscode"
import { AutocompleteModel } from "../AutocompleteModel"
-import { AutocompleteContext, VisibleCodeContext } from "../types"
+import type { AutocompleteContext, VisibleCodeContext } from "../types"
import { removePrefixOverlap } from "../continuedev/core/autocomplete/postprocessing/removePrefixOverlap.js"
import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
import { postprocessAutocompleteSuggestion } from "../classic-auto-complete/uselessSuggestionFilter"
import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
import { FileIgnoreController } from "../shims/FileIgnoreController"
import type { KiloConnectionService } from "../../cli-backend"
-import type { ChatCompletionRequestMessage, ChatCompletionResponseSender } from "./handleChatCompletionRequest"
import { finalizeChatSuggestion, buildChatPrefix } from "./chat-autocomplete-utils"
+interface ChatCompletionRequestMessage {
+ type: "requestChatCompletion"
+ text: string
+ requestId: string
+}
+
+interface ChatCompletionResponseSender {
+ postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void
+}
+
/**
* Chat textarea autocomplete with cached per-request objects.
*
diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts
deleted file mode 100644
index 9bb9a153bd1..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
-
-export interface ChatCompletionAcceptedMessage {
- type: "chatCompletionAccepted"
- suggestionLength?: number
-}
-
-// Singleton telemetry instance for chat-textarea autocomplete
-// This ensures we use the same instance across requests and acceptance events
-let telemetryInstance: AutocompleteTelemetry | null = null
-
-/**
- * Get or create the telemetry instance for chat-textarea autocomplete
- */
-export function getChatAutocompleteTelemetry(): AutocompleteTelemetry {
- if (!telemetryInstance) {
- telemetryInstance = new AutocompleteTelemetry("chat-textarea")
- }
- return telemetryInstance
-}
-
-/**
- * Handles a chat completion accepted event from the webview.
- * Captures telemetry when the user accepts a suggestion via Tab or ArrowRight.
- */
-export function handleChatCompletionAccepted(message: ChatCompletionAcceptedMessage): void {
- const telemetry = getChatAutocompleteTelemetry()
- telemetry.captureAcceptSuggestion(message.suggestionLength)
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts
deleted file mode 100644
index eeac049683e..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as vscode from "vscode"
-import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
-import { FileIgnoreController } from "../shims/FileIgnoreController"
-import { ChatTextAreaAutocomplete } from "./ChatTextAreaAutocomplete"
-import type { KiloConnectionService } from "../../cli-backend"
-
-export interface ChatCompletionRequestMessage {
- type: "requestChatCompletion"
- text?: string
- requestId?: string
-}
-
-export interface ChatCompletionResponseSender {
- postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void
-}
-
-/**
- * Handles a chat completion request from the webview.
- * Captures visible code context and generates an autocomplete suggestion.
- */
-export async function handleChatCompletionRequest(
- message: ChatCompletionRequestMessage,
- responseSender: ChatCompletionResponseSender,
- connectionService: KiloConnectionService,
-): Promise {
- const userText = message.text || ""
- const requestId = message.requestId || ""
-
- const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""
-
- const ignoreController = new FileIgnoreController(workspacePath)
- await ignoreController.initialize()
-
- const tracker = new VisibleCodeTracker(workspacePath, ignoreController)
- const visibleContext = await tracker.captureVisibleCode()
-
- const autocomplete = new ChatTextAreaAutocomplete(connectionService)
- const { suggestion } = await autocomplete.getCompletion(userText, visibleContext)
-
- responseSender.postMessage({ type: "chatCompletionResult", text: suggestion, requestId })
-
- ignoreController.dispose()
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/API_REFERENCE.md b/packages/kilo-vscode/src/services/autocomplete/continuedev/API_REFERENCE.md
deleted file mode 100644
index 216538b0c7f..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/API_REFERENCE.md
+++ /dev/null
@@ -1,807 +0,0 @@
-# API Reference
-
-Complete API documentation for the Autocomplete & NextEdit library.
-
-## Table of Contents
-
-- [CompletionProvider](#completionprovider)
-- [NextEditProvider](#nexteditprovider)
-- [MinimalConfigProvider](#minimalconfigprovider)
-- [Core Interfaces](#core-interfaces)
-- [LLM Adapters](#llm-adapters)
-- [Types and Interfaces](#types-and-interfaces)
-
----
-
-## CompletionProvider
-
-The main class for providing AI-powered code autocompletion.
-
-**Location**: [`core/autocomplete/CompletionProvider.ts`](core/autocomplete/CompletionProvider.ts)
-
-### Constructor
-
-```typescript
-constructor(
- configHandler: MinimalConfigProvider,
- ide: IDE,
- _injectedGetLlm: () => Promise,
- _onError: (e: any) => void,
- getDefinitionsFromLsp: GetLspDefinitionsFunction
-)
-```
-
-**Parameters**:
-
-- `configHandler`: Configuration provider for autocomplete options
-- `ide`: IDE interface implementation for file I/O and editor operations
-- `_injectedGetLlm`: Async function that returns the LLM to use for completions
-- `_onError`: Error callback function for handling autocomplete errors
-- `getDefinitionsFromLsp`: Function to retrieve LSP definitions for enhanced context
-
-### Methods
-
-#### `provideInlineCompletionItems()`
-
-Generates an autocomplete completion for the given input.
-
-```typescript
-async provideInlineCompletionItems(
- input: AutocompleteInput,
- token: AbortSignal | undefined,
- force?: boolean
-): Promise
-```
-
-**Parameters**:
-
-- `input`: Autocomplete context including file path, cursor position, recent edits
-- `token`: AbortSignal to cancel the request
-- `force`: If true, bypasses debouncing
-
-**Returns**: `AutocompleteOutcome` containing the completion text and metadata, or `undefined` if no completion
-
-**Example**:
-
-```typescript
-const outcome = await completionProvider.provideInlineCompletionItems(
- {
- filepath: "/path/to/file.ts",
- pos: { line: 10, character: 5 },
- completionId: "unique-completion-id",
- recentlyEditedRanges: [],
- recentlyEditedFiles: new Map(),
- clipboardText: "",
- },
- abortController.signal,
-)
-```
-
-#### `accept()`
-
-Marks a completion as accepted by the user.
-
-```typescript
-accept(completionId: string): void
-```
-
-**Parameters**:
-
-- `completionId`: Unique identifier for the accepted completion
-
-**Side Effects**: Updates bracket matching service and completion cache
-
-#### `markDisplayed()`
-
-Marks a completion as having been displayed to the user.
-
-```typescript
-markDisplayed(completionId: string, outcome: AutocompleteOutcome): void
-```
-
-**Parameters**:
-
-- `completionId`: Unique identifier for the completion
-- `outcome`: The autocomplete outcome that was displayed
-
-#### `cancel()`
-
-Cancels any in-progress autocomplete requests.
-
-```typescript
-cancel(): void
-```
-
-### Configuration Options
-
-See [`MinimalConfigProvider`](#minimalconfigprovider) for configuration options.
-
----
-
-## NextEditProvider
-
-The main class for providing predictive multi-location code edits.
-
-**Location**: [`core/nextEdit/NextEditProvider.ts`](core/nextEdit/NextEditProvider.ts)
-
-### Constructor
-
-```typescript
-constructor(
- configHandler: MinimalConfigProvider,
- ide: IDE,
- _injectedGetLlm: () => Promise,
- _onError: (e: any) => void
-)
-```
-
-**Parameters**:
-
-- `configHandler`: Configuration provider
-- `ide`: IDE interface implementation
-- `_injectedGetLlm`: Function returning the LLM for edit predictions
-- `_onError`: Error callback
-
-### Methods
-
-#### `getNextEditPrediction()`
-
-Generates predicted edits based on context and recent changes.
-
-```typescript
-async getNextEditPrediction(
- context: ModelSpecificContext,
- signal?: AbortSignal,
- usingFullFileDiff?: boolean
-): Promise
-```
-
-**Parameters**:
-
-- `context`: Context including file contents, cursor position, recent edits
-- `signal`: Optional AbortSignal to cancel the request
-- `usingFullFileDiff`: If true, generates full-file diffs; if false, only edits within a region
-
-**Returns**: `NextEditOutcome` containing predicted edits and final cursor position
-
-**Example**:
-
-```typescript
-const outcome = await nextEditProvider.getNextEditPrediction(
- {
- filepath: "/path/to/file.ts",
- pos: { line: 15, character: 0 },
- fileContents: currentFileContents,
- userEdits: recentDiff,
- // ... other context
- },
- abortController.signal,
- false, // Use partial file diff
-)
-
-if (outcome) {
- console.log("Edit regions:", outcome.editableRegions)
- console.log("Diff lines:", outcome.diffLines)
- console.log("New cursor:", outcome.finalCursorPosition)
-}
-```
-
----
-
-## MinimalConfigProvider
-
-Simple configuration provider that replaces the complex Continue config system.
-
-**Location**: [`core/autocomplete/MinimalConfig.ts`](core/autocomplete/MinimalConfig.ts)
-
-### Constructor
-
-```typescript
-constructor(config?: Partial)
-```
-
-**Parameters**:
-
-- `config`: Optional partial configuration to override defaults
-
-**Example**:
-
-```typescript
-const configProvider = new MinimalConfigProvider({
- tabAutocompleteOptions: {
- debounceDelay: 200,
- maxPromptTokens: 2048,
- prefixPercentage: 0.5,
- suffixPercentage: 0.3,
- useCache: true,
- onlyMyCode: false,
- },
- experimental: {
- enableStaticContextualization: true,
- },
-})
-```
-
-### Methods
-
-#### `loadConfig()`
-
-Returns the configuration object.
-
-```typescript
-async loadConfig(): Promise<{ config: MinimalConfig }>
-```
-
-**Returns**: Promise resolving to an object containing the config
-
-#### `getAutocompleteOptions()`
-
-Gets autocomplete-specific options.
-
-```typescript
-getAutocompleteOptions(): TabAutocompleteOptions
-```
-
-**Returns**: Autocomplete configuration options
-
-#### `isStaticContextualizationEnabled()`
-
-Checks if static contextualization is enabled.
-
-```typescript
-isStaticContextualizationEnabled(): boolean
-```
-
-**Returns**: True if enabled, false otherwise
-
-### Configuration Interface
-
-```typescript
-interface MinimalConfig {
- tabAutocompleteOptions?: TabAutocompleteOptions
- experimental?: {
- enableStaticContextualization?: boolean
- }
- modelsByRole?: {
- autocomplete?: ILLM[]
- }
- selectedModelByRole?: {
- autocomplete?: ILLM
- }
-}
-```
-
-### TabAutocompleteOptions
-
-```typescript
-interface TabAutocompleteOptions {
- debounceDelay?: number // Debounce delay in ms (default: 150)
- maxPromptTokens?: number // Max tokens for prompt (default: 1024)
- prefixPercentage?: number // Percentage of tokens for prefix (default: 0.5)
- suffixPercentage?: number // Percentage of tokens for suffix (default: 0.3)
- maxSuffixPercentage?: number // Max suffix tokens percentage (default: 0.5)
- useCache?: boolean // Enable completion caching (default: true)
- onlyMyCode?: boolean // Only use workspace files for context (default: false)
- template?: string // Custom prompt template
- useFileSuffix?: boolean // Include file suffix in context (default: true)
- multilineCompletions?: "always" | "never" | "auto" // Multiline behavior (default: 'auto')
- slidingWindowPrefixPercentage?: number // Sliding window prefix % (default: 0.75)
- slidingWindowSize?: number // Sliding window size (default: 500)
- maxSnippetPercentage?: number // Max tokens for snippets (default: 0.6)
- recentlyEditedSimilarityThreshold?: number // Similarity threshold (default: 0.3)
- useOtherFiles?: boolean // Use other files for context (default: true)
- disableInFiles?: string[] // Glob patterns to disable autocomplete
- stopTokens?: string[] // Custom stop tokens
- tokensPerCompletion?: number // Tokens per completion (default: 256)
- transform?: boolean // Apply post-processing transforms (default: true)
-}
-```
-
----
-
-## Core Interfaces
-
-### IDE Interface
-
-The IDE interface abstracts editor operations. Implement this to integrate with your editor.
-
-**Location**: [`core/index.d.ts`](core/index.d.ts)
-
-```typescript
-interface IDE {
- // File Operations
- readFile(filepath: string): Promise
- writeFile(filepath: string, contents: string): Promise
-
- // Workspace
- getWorkspaceDirs(): Promise
- listWorkspaceContents(directory?: string): Promise
-
- // Editor State
- getCurrentFile(): Promise
- getCursorPosition(): Promise
- getVisibleFiles(): Promise
-
- // Code Navigation
- getDefinition(filepath: string, position: Position): Promise
- getReferences(filepath: string, position: Position): Promise
- getSymbols(filepath: string): Promise
-
- // File Information
- readRangeInFile(filepath: string, range: Range): Promise
- getStats(filepath: string): Promise
-
- // Edits
- applyEdits(edits: FileEdit[]): Promise
-
- // Diff/SCM
- getDiff(includeUnstaged: boolean): Promise
- getRepoName(dir: string): Promise
- getBranch(dir: string): Promise
-
- // UI
- showMessage(message: string, severity?: "info" | "warning" | "error"): Promise
- showToast(type: "info" | "warning" | "error", message: string, ...actions: string[]): Promise
-
- // Terminal
- runCommand(command: string, options?: TerminalOptions): Promise
-
- // Clipboard
- getClipboardContent(): Promise<{ text: string; copiedAt: number } | undefined>
-
- // Search
- getSearchResults(query: string): Promise
- subprocess(command: string, cwd?: string): Promise<[string, string]>
-
- // Other
- getIdeInfo(): Promise
- getIdeSettings(): Promise
- isTelemetryEnabled(): Promise
- getUniqueId(): Promise
-}
-```
-
-**Key Methods to Implement**:
-
-- `readFile()`, `writeFile()`: Essential for file I/O
-- `getWorkspaceDirs()`: Returns workspace root directories
-- `getCurrentFile()`, `getCursorPosition()`: Current editor state
-- `applyEdits()`: Apply code changes
-- `getDefinition()`: LSP-like functionality for context gathering
-
-### ILLM Interface
-
-The ILLM (Language Model) interface abstracts LLM providers.
-
-**Location**: [`core/index.d.ts`](core/index.d.ts)
-
-```typescript
-interface ILLM {
- // Required properties
- uniqueId: string
- model: string
- contextLength: number
- completionOptions: CompletionOptions
-
- // Provider info
- get providerName(): string
- get underlyingProviderName(): string
-
- // Optional
- apiKey?: string
- apiBase?: string
- autocompleteOptions?: Partial
- promptTemplates?: PromptTemplates
-
- // Completion methods
- complete(prompt: string, signal: AbortSignal, options?: LLMFullCompletionOptions): Promise
-
- streamComplete(
- prompt: string,
- signal: AbortSignal,
- options?: LLMFullCompletionOptions,
- ): AsyncGenerator
-
- streamFim(
- prefix: string,
- suffix: string,
- signal: AbortSignal,
- options?: LLMFullCompletionOptions,
- ): AsyncGenerator
-
- // Chat methods
- chat(messages: ChatMessage[], signal: AbortSignal, options?: LLMFullCompletionOptions): Promise
-
- streamChat(
- messages: ChatMessage[],
- signal: AbortSignal,
- options?: LLMFullCompletionOptions,
- ): AsyncGenerator
-
- // Utility methods
- countTokens(text: string): number
- supportsImages(): boolean
- supportsCompletions(): boolean
- supportsFim(): boolean
- listModels(): Promise
-}
-```
-
----
-
-## LLM Adapters
-
-### OpenAI
-
-Pre-built adapter for OpenAI and OpenAI-compatible APIs.
-
-**Location**: [`core/llm/llms/OpenAI.ts`](core/llm/llms/OpenAI.ts)
-
-```typescript
-import OpenAI from "@continuedev/core/llm/llms/OpenAI"
-
-const llm = new OpenAI({
- model: "gpt-4",
- apiKey: process.env.OPENAI_API_KEY,
- apiBase: "https://api.openai.com/v1", // Optional custom base URL
- completionOptions: {
- temperature: 0.1,
- maxTokens: 1000,
- },
-})
-```
-
-**Constructor Options**:
-
-```typescript
-interface OpenAIOptions {
- model: string
- apiKey: string
- apiBase?: string
- completionOptions?: CompletionOptions
- contextLength?: number
- autocompleteOptions?: Partial
-}
-```
-
-### Creating Custom LLM Adapters
-
-To create a custom LLM adapter, implement the `ILLM` interface:
-
-```typescript
-import { ILLM, CompletionOptions } from "@continuedev/core"
-
-class CustomLLM implements ILLM {
- uniqueId = "custom-llm"
- model: string
- contextLength: number
- completionOptions: CompletionOptions
-
- get providerName() {
- return "custom"
- }
- get underlyingProviderName() {
- return "custom"
- }
-
- constructor(options: { model: string }) {
- this.model = options.model
- this.contextLength = 4096
- this.completionOptions = {
- model: options.model,
- temperature: 0.1,
- }
- }
-
- async complete(prompt: string, signal: AbortSignal): Promise {
- // Call your LLM API
- const response = await fetch("your-api-endpoint", {
- method: "POST",
- body: JSON.stringify({ prompt }),
- signal,
- })
- return await response.text()
- }
-
- async *streamComplete(prompt: string, signal: AbortSignal) {
- // Stream from your LLM API
- for await (const chunk of streamFromAPI(prompt, signal)) {
- yield chunk
- }
- return { modelTitle: this.model, prompt, completion: "" }
- }
-
- // Implement other required methods...
- countTokens(text: string): number {
- return text.length / 4 // Rough estimate
- }
-
- supportsImages() {
- return false
- }
- supportsCompletions() {
- return true
- }
- supportsFim() {
- return false
- }
-
- // ... other methods
-}
-```
-
----
-
-## Types and Interfaces
-
-### AutocompleteInput
-
-Input for autocomplete requests.
-
-```typescript
-interface AutocompleteInput {
- filepath: string // Path to the file being edited
- pos: Position // Cursor position
- completionId: string // Unique ID for this completion request
- recentlyEditedRanges: Range[] // Recently edited ranges in this file
- recentlyEditedFiles: Map // Recently edited files
- clipboardText: string // Current clipboard content
- manuallyPassFileContext?: RangeInFile[] // Manually provided context
-}
-```
-
-### AutocompleteOutcome
-
-Result of an autocomplete request.
-
-```typescript
-interface AutocompleteOutcome {
- completion: string // The completion text
- completionId: string // Unique ID for this completion
- filepath: string // File path
- prefix: string // Code prefix (before cursor)
- suffix: string // Code suffix (after cursor)
- prompt: string // Full prompt sent to LLM
- modelTitle: string // Model used
- modelProvider: string // Provider used
- completionOptions: CompletionOptions // Options used
- cacheHit: boolean // Whether cached
- latency: number // Response latency in ms
-}
-```
-
-### NextEditOutcome
-
-Result of a NextEdit prediction.
-
-```typescript
-interface NextEditOutcome {
- edits: string // The predicted edit text
- diffLines: DiffLine[] // Diff representation
- editableRegions: Range[] // Regions that were edited
- finalCursorPosition: Position // Predicted cursor position after edits
- filepath: string // File path
- prompt: string // Prompt sent to LLM
- modelTitle: string // Model used
- latency: number // Response latency in ms
-}
-```
-
-### Position
-
-Position in a text document.
-
-```typescript
-interface Position {
- line: number // 0-based line number
- character: number // 0-based character offset
-}
-```
-
-### Range
-
-Range in a text document.
-
-```typescript
-interface Range {
- start: Position // Start position (inclusive)
- end: Position // End position (exclusive)
-}
-```
-
-### RangeInFile
-
-Range in a specific file.
-
-```typescript
-interface RangeInFile {
- filepath: string // File path
- range: Range // Range within the file
-}
-```
-
-### DiffLine
-
-A single line in a diff.
-
-```typescript
-interface DiffLine {
- type: "same" | "new" | "old" // Type of change
- line: string // Line content
- lineNumber: number // Line number in file
-}
-```
-
-### FileEdit
-
-An edit to apply to a file.
-
-```typescript
-interface FileEdit {
- filepath: string // File to edit
- range: Range // Range to replace
- replacement: string // New content
-}
-```
-
----
-
-## Usage Examples
-
-### Complete Example: Autocomplete with Custom IDE
-
-```typescript
-import { CompletionProvider, MinimalConfigProvider } from "@continuedev/core/autocomplete"
-import { IDE, ILLM, Position } from "@continuedev/core"
-import OpenAI from "@continuedev/core/llm/llms/OpenAI"
-
-// 1. Implement IDE interface
-class MyIDE implements IDE {
- async readFile(filepath: string): Promise {
- return fs.readFileSync(filepath, "utf-8")
- }
-
- async getWorkspaceDirs(): Promise {
- return ["/path/to/workspace"]
- }
-
- async getCurrentFile() {
- return {
- filepath: this.currentFilePath,
- contents: await this.readFile(this.currentFilePath),
- }
- }
-
- async getCursorPosition(): Promise {
- return this.cursorPosition
- }
-
- // ... implement other methods
-}
-
-// 2. Set up configuration
-const config = new MinimalConfigProvider({
- tabAutocompleteOptions: {
- debounceDelay: 150,
- maxPromptTokens: 1024,
- useCache: true,
- },
-})
-
-// 3. Set up LLM
-const getLlm = async (): Promise => {
- return new OpenAI({
- apiKey: process.env.OPENAI_API_KEY,
- model: "gpt-4",
- })
-}
-
-// 4. Create completion provider
-const ide = new MyIDE()
-const provider = new CompletionProvider(
- config,
- ide,
- getLlm,
- (error) => console.error(error),
- async () => [], // LSP definitions function
-)
-
-// 5. Request completion
-const outcome = await provider.provideInlineCompletionItems(
- {
- filepath: "/path/to/file.ts",
- pos: { line: 10, character: 5 },
- completionId: "completion-1",
- recentlyEditedRanges: [],
- recentlyEditedFiles: new Map(),
- clipboardText: "",
- },
- new AbortController().signal,
-)
-
-if (outcome) {
- console.log("Completion:", outcome.completion)
- provider.markDisplayed("completion-1", outcome)
-}
-```
-
----
-
-## Error Handling
-
-Both `CompletionProvider` and `NextEditProvider` accept an error callback:
-
-```typescript
-const onError = (error: any) => {
- if (error instanceof Error) {
- console.error("Error:", error.message)
- // Show user notification
- showNotification(error.message)
- }
-}
-
-const provider = new CompletionProvider(
- config,
- ide,
- getLlm,
- onError, // Error callback
- getLspDefinitions,
-)
-```
-
-Common errors:
-
-- **LLM API errors**: Network failures, invalid API keys, rate limits
-- **File I/O errors**: Missing files, permission errors
-- **Timeout errors**: Long-running requests that are aborted
-
----
-
-## Performance Considerations
-
-### Caching
-
-Completions are automatically cached using an LRU cache. Configure caching:
-
-```typescript
-const config = new MinimalConfigProvider({
- tabAutocompleteOptions: {
- useCache: true, // Enable caching (default)
- },
-})
-```
-
-### Debouncing
-
-Prevent excessive LLM calls during rapid typing:
-
-```typescript
-const config = new MinimalConfigProvider({
- tabAutocompleteOptions: {
- debounceDelay: 150, // Wait 150ms before requesting (default)
- },
-})
-```
-
-### Abort Signals
-
-Always provide AbortSignals to cancel in-progress requests:
-
-```typescript
-const controller = new AbortController()
-
-// Start request
-const promise = provider.provideInlineCompletionItems(input, controller.signal)
-
-// Cancel if needed
-controller.abort()
-```
-
----
-
-## See Also
-
-- [README.md](README.md) - Project overview and quick start
-- [ARCHITECTURE.md](ARCHITECTURE.md) - Technical architecture details
-- [EXAMPLES.md](EXAMPLES.md) - More usage examples
-- [Core TypeScript Definitions](core/index.d.ts) - Full type definitions
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts
deleted file mode 100644
index 8871fc5e0fb..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-import { MinimalConfigProvider } from "./MinimalConfig.js"
-import { IDE, ILLM } from "../index.js"
-import { DEFAULT_AUTOCOMPLETE_OPTS } from "../util/parameters.js"
-import { shouldCompleteMultiline } from "./classification/shouldCompleteMultiline.js"
-import { ContextRetrievalService } from "./context/ContextRetrievalService.js"
-import { isSecurityConcern } from "../indexing/ignore.js"
-import { BracketMatchingService } from "./filtering/BracketMatchingService.js"
-import { CompletionStreamer } from "./generation/CompletionStreamer.js"
-import { postprocessCompletion } from "./postprocessing/index.js"
-import { shouldPrefilter } from "./prefiltering/index.js"
-import { getAllSnippetsWithoutRace } from "./snippets/index.js"
-import { renderPromptWithTokenLimit } from "./templating/index.js"
-import { GetLspDefinitionsFunction } from "./types.js"
-import { AutocompleteDebouncer } from "./util/AutocompleteDebouncer.js"
-import { AutocompleteLoggingService } from "./util/AutocompleteLoggingService.js"
-import { AutocompleteLruCacheInMem } from "./util/AutocompleteLruCacheInMem.js"
-import { HelperVars } from "./util/HelperVars.js"
-import { AutocompleteInput, AutocompleteOutcome } from "./util/types.js"
-
-// Errors that can be expected on occasion even during normal functioning should not be shown.
-// Not worth disrupting the user to tell them that a single autocomplete request didn't go through
-const ERRORS_TO_IGNORE = [
- // From Ollama
- "unexpected server status",
- "operation was aborted",
-]
-
-export class CompletionProvider {
- private autocompleteCache = AutocompleteLruCacheInMem.get()
- public errorsShown: Set = new Set()
- private bracketMatchingService = new BracketMatchingService()
- private debouncer = new AutocompleteDebouncer()
- private completionStreamer: CompletionStreamer
- private loggingService = new AutocompleteLoggingService()
- private contextRetrievalService: ContextRetrievalService
-
- constructor(
- private readonly configHandler: MinimalConfigProvider,
- private readonly ide: IDE,
- private readonly _injectedGetLlm: () => Promise,
- private readonly _onError: (e: unknown) => void,
- private readonly getDefinitionsFromLsp: GetLspDefinitionsFunction,
- ) {
- this.completionStreamer = new CompletionStreamer(this.onError.bind(this))
- this.contextRetrievalService = new ContextRetrievalService(this.ide)
- }
-
- private async _prepareLlm(): Promise {
- const llm = await this._injectedGetLlm()
-
- if (!llm) {
- return undefined
- }
-
- // Temporary fix for JetBrains autocomplete bug as described in https://github.com/continuedev/continue/pull/3022
- if (llm.model === undefined && llm.completionOptions?.model !== undefined) {
- llm.model = llm.completionOptions.model
- }
-
- // Ignore empty API keys for Mistral since we currently write
- // a template provider without one during onboarding
- if (llm.providerName === "mistral" && llm.apiKey === "") {
- return undefined
- }
-
- // Set temperature (but don't override)
- if (llm.completionOptions.temperature === undefined) {
- llm.completionOptions.temperature = 0.01
- }
-
- return llm
- }
-
- private onError(e: unknown) {
- if (
- ERRORS_TO_IGNORE.some((err) => (typeof e === "string" ? e.includes(err) : (e as Error)?.message?.includes(err)))
- ) {
- return
- }
-
- console.warn("Error generating autocompletion: ", e)
- const errorMessage = e instanceof Error ? e.message : String(e)
- if (!this.errorsShown.has(errorMessage)) {
- this.errorsShown.add(errorMessage)
- this._onError(e)
- }
- }
-
- public cancel() {
- this.loggingService.cancel()
- }
-
- public accept(completionId: string) {
- const outcome = this.loggingService.accept(completionId)
- if (!outcome) {
- return
- }
- this.bracketMatchingService.handleAcceptedCompletion(outcome.completion, outcome.filepath)
- }
-
- public markDisplayed(completionId: string, outcome: AutocompleteOutcome) {
- this.loggingService.markDisplayed(completionId, outcome)
- }
-
- private async _getAutocompleteOptions(llm: ILLM) {
- const { config } = await this.configHandler.loadConfig()
- const options = {
- ...DEFAULT_AUTOCOMPLETE_OPTS,
- ...config?.tabAutocompleteOptions,
- ...llm.autocompleteOptions,
- }
-
- // Enable static contextualization if defined.
- if (config?.experimental?.enableStaticContextualization) {
- options.experimental_enableStaticContextualization = false
- }
-
- return options
- }
-
- public async provideInlineCompletionItems(
- input: AutocompleteInput,
- token: AbortSignal | undefined,
- force?: boolean,
- ): Promise {
- try {
- // Create abort signal if not given
- if (!token) {
- const controller = this.loggingService.createAbortController(input.completionId)
- token = controller.signal
- }
- const startTime = Date.now()
-
- const llm = await this._prepareLlm()
- if (!llm) {
- return undefined
- }
-
- if (isSecurityConcern(input.filepath)) {
- return undefined
- }
-
- const options = await this._getAutocompleteOptions(llm)
-
- // Debounce
- if (!force) {
- if (await this.debouncer.delayAndShouldDebounce(options.debounceDelay)) {
- return undefined
- }
- }
-
- const helper = await HelperVars.create(input, options, llm.model, this.ide)
-
- if (await shouldPrefilter(helper, await this.ide.getWorkspaceDirs())) {
- return undefined
- }
-
- const [snippetPayload, workspaceDirs] = await Promise.all([
- getAllSnippetsWithoutRace({
- helper,
- ide: this.ide,
- getDefinitionsFromLsp: this.getDefinitionsFromLsp,
- contextRetrievalService: this.contextRetrievalService,
- }),
- this.ide.getWorkspaceDirs(),
- ])
-
- const { prompt, prefix, suffix, completionOptions } = renderPromptWithTokenLimit({
- snippetPayload,
- workspaceDirs,
- helper,
- llm,
- })
-
- // Completion
- let completion: string | undefined = ""
-
- const cache = await this.autocompleteCache
- const cachedCompletion = helper.options.useCache ? await cache.get(helper.prunedPrefix) : undefined
- let cacheHit = false
- if (cachedCompletion) {
- // Cache
- cacheHit = true
- completion = cachedCompletion
- } else {
- const multiline = !helper.options.transform || shouldCompleteMultiline(helper)
-
- const completionStream = this.completionStreamer.streamCompletionWithFilters(
- token,
- llm,
- prefix,
- suffix,
- prompt,
- multiline,
- completionOptions,
- helper,
- )
-
- for await (const update of completionStream) {
- completion += update
- }
-
- // Don't postprocess if aborted
- if (token.aborted) {
- return undefined
- }
-
- const processedCompletion = helper.options.transform
- ? postprocessCompletion({
- completion,
- prefix: helper.prunedPrefix,
- suffix: helper.prunedSuffix,
- llm,
- })
- : completion
-
- completion = processedCompletion
- }
-
- if (!completion) {
- return undefined
- }
-
- const outcome: AutocompleteOutcome = {
- time: Date.now() - startTime,
- completion,
- prefix,
- suffix,
- prompt,
- modelProvider: llm.underlyingProviderName,
- modelName: llm.model,
- completionOptions,
- cacheHit,
- filepath: helper.filepath,
- numLines: completion.split("\n").length,
- completionId: helper.input.completionId,
- gitRepo: "fake-placeholder", //MINIMAL_REPO - came from git
- uniqueId: await this.ide.getUniqueId(),
- timestamp: new Date().toISOString(),
- profileType: this.configHandler.currentProfile?.profileDescription.profileType,
- ...helper.options,
- }
-
- if (options.experimental_enableStaticContextualization) {
- outcome.enabledStaticContextualization = true
- }
-
- //////////
- // Save to cache
- if (!outcome.cacheHit && helper.options.useCache) {
- ;(await this.autocompleteCache)
- .put(outcome.prefix, outcome.completion)
- .catch((e) => console.warn(`Failed to save to cache: ${e.message}`))
- }
-
- // When using the JetBrains extension, Mark as displayed
- const ideType = (await this.ide.getIdeInfo()).ideType
- if (ideType === "jetbrains") {
- this.markDisplayed(input.completionId, outcome)
- }
-
- return outcome
- } catch (e: unknown) {
- this.onError(e)
- return undefined
- } finally {
- this.loggingService.deleteAbortController(input.completionId)
- }
- }
-}
-
-export default CompletionProvider
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/MinimalConfig.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/MinimalConfig.ts
deleted file mode 100644
index c834949ff15..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/MinimalConfig.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Minimal configuration for autocomplete features.
- * This replaces the complex ConfigHandler system with simple hardcoded defaults.
- *
- * Analysis of ConfigHandler usage:
- * - CompletionProvider needs: config.tabAutocompleteOptions, config.experimental.enableStaticContextualization, currentProfile.profileType
- *
- * The profileType is only used for logging/telemetry, so we can set it to undefined for a minimal extraction.
- */
-
-import { ILLM, TabAutocompleteOptions } from "../index.js"
-import { DEFAULT_AUTOCOMPLETE_OPTS } from "../util/parameters.js"
-
-interface MinimalConfig {
- tabAutocompleteOptions?: TabAutocompleteOptions
- experimental?: {
- enableStaticContextualization?: boolean
- }
- // Minimal model selection support for NextEdit context fetching
- modelsByRole?: {
- autocomplete?: ILLM[]
- }
- selectedModelByRole?: {
- autocomplete?: ILLM
- edit?: ILLM
- chat?: ILLM
- rerank?: ILLM
- }
- rules?: unknown[]
-}
-
-interface MinimalProfile {
- profileDescription: {
- profileType?: "control-plane" | "local" | "platform"
- }
-}
-
-/**
- * Default configuration with hardcoded values suitable for autocomplete/NextEdit.
- * Uses the same defaults from DEFAULT_AUTOCOMPLETE_OPTS.
- */
-const DEFAULT_MINIMAL_CONFIG: MinimalConfig = {
- tabAutocompleteOptions: {
- ...DEFAULT_AUTOCOMPLETE_OPTS,
- },
- experimental: {
- enableStaticContextualization: false,
- },
- modelsByRole: {
- autocomplete: [],
- },
- selectedModelByRole: {
- autocomplete: undefined,
- },
-}
-
-/**
- * Simple config provider that replaces ConfigHandler for autocomplete/NextEdit.
- * Returns hardcoded configuration without dependencies on control-plane.
- */
-export class MinimalConfigProvider {
- private config: MinimalConfig
- public currentProfile: MinimalProfile | undefined
-
- constructor(config?: Partial) {
- this.config = {
- ...DEFAULT_MINIMAL_CONFIG,
- ...config,
- tabAutocompleteOptions: {
- ...DEFAULT_AUTOCOMPLETE_OPTS,
- ...config?.tabAutocompleteOptions,
- } as TabAutocompleteOptions,
- experimental: {
- ...DEFAULT_MINIMAL_CONFIG.experimental,
- ...config?.experimental,
- },
- }
-
- // Set a minimal profile for logging purposes
- // In a minimal extraction, we don't have a control-plane profile
- this.currentProfile = undefined
- }
-
- /**
- * Returns the config in the same shape as ConfigHandler.loadConfig()
- * This maintains API compatibility with existing code.
- */
- async loadConfig(): Promise<{ config: MinimalConfig }> {
- return { config: this.config }
- }
-
- /**
- * Get autocomplete options directly
- */
- getAutocompleteOptions(): TabAutocompleteOptions {
- return this.config.tabAutocompleteOptions || DEFAULT_AUTOCOMPLETE_OPTS
- }
-
- /**
- * Check if static contextualization is enabled
- */
- isStaticContextualizationEnabled(): boolean {
- return this.config.experimental?.enableStaticContextualization ?? false
- }
-
- /**
- * Reload config (stub for compatibility)
- */
- async reloadConfig(..._args: unknown[]): Promise {
- // No-op for minimal config
- }
-
- /**
- * Register config update handler (stub for compatibility)
- */
- onConfigUpdate(_handler: (event: { config: MinimalConfig; configLoadInterrupted: boolean }) => void): void {
- // No-op for minimal config
- }
-
- /**
- * Register custom context provider (stub for compatibility)
- */
- registerCustomContextProvider(_provider: unknown): void {
- // No-op for minimal config
- }
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/classification/shouldCompleteMultiline.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/classification/shouldCompleteMultiline.ts
deleted file mode 100644
index e24b7008402..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/classification/shouldCompleteMultiline.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { AutocompleteLanguageInfo } from "../constants/AutocompleteLanguageInfo"
-import { HelperVars } from "../util/HelperVars"
-
-function shouldCompleteMultilineBasedOnLanguage(language: AutocompleteLanguageInfo, prefix: string, suffix: string) {
- return language.useMultiline?.({ prefix, suffix }) ?? true
-}
-
-export function shouldCompleteMultiline(helper: HelperVars) {
- switch (helper.options.multilineCompletions) {
- case "always":
- return true
- case "never":
- return false
- default:
- break
- }
-
- // Always single-line if an intellisense option is selected
- if (helper.input.selectedCompletionInfo) {
- return true
- }
-
- // // Don't complete multi-line if you are mid-line
- // if (isMidlineCompletion(helper.fullPrefix, helper.fullSuffix)) {
- // return false;
- // }
-
- // Don't complete multi-line for single-line comments
- if (
- helper.lang.singleLineComment &&
- helper.fullPrefix.split("\n").slice(-1)[0]?.trimStart().startsWith(helper.lang.singleLineComment)
- ) {
- return false
- }
-
- return shouldCompleteMultilineBasedOnLanguage(helper.lang, helper.prunedPrefix, helper.prunedSuffix)
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts
deleted file mode 100644
index cb853ef39b6..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// @ts-nocheck
-
-const getAddress = (person: Person): Address => {
- // TODO
-}
-
-const logPerson = (person: Person) => {
- // TODO
-}
-
-const getHardcodedAddress = (): Address => {
- // TODO
-}
-
-const getAddresses = (people: Person[]): Address[] => {
- // TODO
-}
-
-const logPersonWithAddres = (person: Person): Person => {
- // TODO
-}
-
-const logPersonOrAddress = (person: Person | Address): Person | Address => {
- // TODO
-}
-
-const logPersonAndAddress = (person: Person, address: Address) => {
- // TODO
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts
deleted file mode 100644
index 08f7506a70e..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// @ts-nocheck
-
-class Group {
- getPersonAddress(person: Person): Address {
- // TODO
- }
-
- getHardcodedAddress(): Address {
- // TODO
- }
-
- addPerson(person: Person) {
- // TODO
- }
-
- addPeople(people: Person[]) {
- // TODO
- }
-
- getAddresses(people: Person[]): Address[] {
- // TODO
- }
-
- logPersonWithAddress(person: Person): Person {
- // TODO
- }
-
- logPersonOrAddress(person: Person | Address): Person | Address {
- // TODO
- }
-
- logPersonAndAddress(person: Person, address: Address) {
- // TODO
- }
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classes.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classes.ts
deleted file mode 100644
index 8bb8299868e..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classes.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// @ts-nocheck
-
-class Group extends BaseClass {}
-
-class Group implements FirstInterface {}
-
-class Group extends BaseClass implements FirstInterface, SecondInterface {}
-
-class Group extends BaseClass implements FirstInterface {}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts
deleted file mode 100644
index bb9d39c85d9..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// @ts-nocheck
-
-function getAddress(person: Person): Address {
- // TODO
-}
-
-function getFirstAddress(people: Person[]): Address {
- // TODO
-}
-
-function logPerson(person: Person) {
- // TODO
-}
-
-function getHardcodedAddress(): Address {
- // TODO
-}
-
-function getAddresses(people: Person[]): Address[] {
- // TODO
-}
-
-function logPersonWithAddress(person: Person): Person {
- // TODO
-}
-
-function logPersonOrAddress(person: Person | Address): Person | Address {
- // TODO
-}
-
-function logPersonAndAddress(person: Person, address: Address) {
- // TODO
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts
deleted file mode 100644
index 79811b47873..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// @ts-nocheck
-
-function* getAddress(person: Person): Address {
- // TODO
-}
-
-function* getFirstAddress(people: Person[]): Address {
- // TODO
-}
-
-function* logPerson(person: Person) {
- // TODO
-}
-
-function* getHardcodedAddress(): Address {
- // TODO
-}
-
-function* getAddresses(people: Person[]): Address[] {
- // TODO
-}
-
-function* logPersonWithAddress(person: Person): Person {
- // TODO
-}
-
-function* logPersonOrAddress(person: Person | Address): Person | Address {
- // TODO
-}
-
-function* logPersonAndAddress(person: Person, address: Address) {
- // TODO
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__test-cases__/python.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__test-cases__/python.ts
index 68a29efbfa1..cb1c48318c1 100644
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__test-cases__/python.ts
+++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/__test-cases__/python.ts
@@ -1,98 +1,3 @@
-export const FUNCTIONS = [
- {
- nodeType: "function_definition with argument and return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 15, character: 8 },
- definitionPositions: [
- { row: 14, column: 30 }, // Person
- { row: 14, column: 42 }, // Address
- ],
- },
- {
- nodeType: "function_definition with generic argument and generic return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 18, character: 8 },
- definitionPositions: [
- { row: 17, column: 35 }, // Group
- { row: 17, column: 42 }, // Person
- { row: 17, column: 53 }, // Group
- { row: 17, column: 61 }, // Address
- ],
- },
- {
- nodeType: "function_definition with single argument and None return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 21, character: 8 },
- definitionPositions: [
- { row: 20, column: 29 }, // Person
- ],
- },
- {
- nodeType: "function_definition with no arguments and single return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 24, character: 8 },
- definitionPositions: [
- { row: 23, column: 38 }, // Address
- ],
- },
- {
- nodeType: "function_definition with Union arguments and Union return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 27, character: 8 },
- definitionPositions: [
- { row: 26, column: 45 }, // Person
- { row: 26, column: 54 }, // Address
- { row: 26, column: 72 }, // Person
- { row: 26, column: 81 }, // Address
- ],
- },
- {
- nodeType: "function_definition with multiple arguments and None return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 30, character: 8 },
- definitionPositions: [
- { row: 29, column: 41 }, // Person
- { row: 29, column: 59 }, // Address
- ],
- },
- {
- nodeType: "function_definition with one argument and Generator return type",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 33, character: 9 },
- definitionPositions: [
- { row: 32, column: 40 }, // Person
- { row: 32, column: 62 }, // Address
- ],
- },
- {
- nodeType: "function_definition inside a class",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 38, character: 12 },
- definitionPositions: [
- { row: 37, column: 51 }, // Person
- { row: 37, column: 69 }, // Address
- ],
- },
- {
- nodeType: "function_definition of an async function",
- fileName: "python/functions.py",
- language: "Python",
- cursorPosition: { line: 41, character: 8 },
- definitionPositions: [
- { row: 40, column: 37 }, // Address
- { row: 40, column: 48 }, // Person
- ],
- },
-]
-
export const CLASSES = [
{
nodeType: "class_definition with multiple superclasses",
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/StreamTransformPipeline.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/StreamTransformPipeline.ts
deleted file mode 100644
index 28bedc73429..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/StreamTransformPipeline.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { streamLines } from "../../../diff/util"
-import { HelperVars } from "../../util/HelperVars"
-
-import { stopAtStartOf, stopAtStopTokens } from "./charStream"
-import {
- avoidEmptyComments,
- avoidPathLine,
- noDoubleNewLine,
- showWhateverWeHaveAtXMs,
- skipPrefixes,
- stopAtLines,
- stopAtLinesExact,
- stopAtRepeatingLines,
- stopAtSimilarLine,
- streamWithNewLines,
-} from "./lineStream"
-
-const STOP_AT_PATTERNS = ["diff --git"]
-
-export class StreamTransformPipeline {
- async *transform(
- generator: AsyncGenerator,
- prefix: string,
- suffix: string,
- multiline: boolean,
- stopTokens: string[],
- fullStop: () => void,
- helper: HelperVars,
- ): AsyncGenerator {
- let charGenerator = generator
-
- charGenerator = stopAtStopTokens(generator, [...stopTokens, ...STOP_AT_PATTERNS])
- charGenerator = stopAtStartOf(charGenerator, suffix)
- for (const charFilter of helper.lang.charFilters ?? []) {
- charGenerator = charFilter({
- chars: charGenerator,
- prefix,
- suffix,
- filepath: helper.filepath,
- multiline,
- })
- }
-
- let lineGenerator = streamLines(charGenerator)
-
- lineGenerator = stopAtLines(lineGenerator, fullStop)
- const lineBelowCursor = this.getLineBelowCursor(helper)
- if (lineBelowCursor.trim() !== "") {
- lineGenerator = stopAtLinesExact(lineGenerator, fullStop, [lineBelowCursor])
- }
- lineGenerator = stopAtRepeatingLines(lineGenerator, fullStop)
- lineGenerator = avoidEmptyComments(lineGenerator, helper.lang.singleLineComment)
- lineGenerator = avoidPathLine(lineGenerator, helper.lang.singleLineComment)
- lineGenerator = skipPrefixes(lineGenerator)
- lineGenerator = noDoubleNewLine(lineGenerator)
-
- for (const lineFilter of helper.lang.lineFilters ?? []) {
- lineGenerator = lineFilter({ lines: lineGenerator, fullStop })
- }
-
- lineGenerator = stopAtSimilarLine(lineGenerator, this.getLineBelowCursor(helper), fullStop)
-
- const timeoutValue = helper.options.modelTimeout
-
- lineGenerator = showWhateverWeHaveAtXMs(lineGenerator, timeoutValue!)
-
- const finalGenerator = streamWithNewLines(lineGenerator)
- for await (const update of finalGenerator) {
- yield update
- }
- }
-
- private getLineBelowCursor(helper: HelperVars): string {
- let lineBelowCursor = ""
- let i = 1
- while (lineBelowCursor.trim() === "" && helper.pos.line + i <= helper.fileLines.length - 1) {
- lineBelowCursor = helper.fileLines[Math.min(helper.pos.line + i, helper.fileLines.length - 1)]
- i++
- }
- return lineBelowCursor
- }
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.test.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.test.ts
deleted file mode 100644
index 488798b60f8..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.test.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { describe, expect, it } from "vitest"
-import { stopAtStartOf, stopAtStopTokens } from "./charStream"
-
-async function* createMockStream(chunks: string[]): AsyncGenerator {
- for (const chunk of chunks) {
- yield chunk
- }
-}
-
-async function streamToString(stream: AsyncGenerator): Promise {
- let result = ""
- for await (const chunk of stream) {
- result += chunk
- }
- return result
-}
-
-describe("stopAtStopTokens", () => {
- it("should yield characters until a stop token is encountered", async () => {
- const mockStream = createMockStream(["Hello", " world", "! Stop", "here"])
- const stopTokens = ["Stop"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- const output = []
- for await (const char of result) {
- output.push(char)
- }
-
- expect(output.join("")).toBe("Hello world! ")
- })
-
- it("should handle multiple stop tokens", async () => {
- const mockStream = createMockStream(["This", " is a ", "test. END", " of stream"])
- const stopTokens = ["END", "STOP", "HALT"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("This is a test. ")
- })
-
- it("should handle stop tokens split across chunks", async () => {
- const mockStream = createMockStream(["Hello", " wo", "r", "ld! ST", "OP now"])
- const stopTokens = ["STOP"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- const output = []
- for await (const char of result) {
- output.push(char)
- }
-
- expect(output.join("")).toBe("Hello world! ")
- })
-
- it("should yield all characters if no stop token is encountered", async () => {
- const mockStream = createMockStream(["This", " is ", "a complete", " stream"])
- const stopTokens = ["END"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("This is a complete stream")
- })
-
- it("should handle empty chunks", async () => {
- const mockStream = createMockStream(["Hello", "", " world", "", "! STOP"])
- const stopTokens = ["STOP"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("Hello world! ")
- })
-
- it("should handle stop token at the beginning of the stream", async () => {
- const mockStream = createMockStream(["STOP", "Hello world"])
- const stopTokens = ["STOP"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- const output = []
- for await (const char of result) {
- output.push(char)
- }
-
- expect(output.join("")).toBe("")
- })
-
- it("should handle stop token at the end of the stream", async () => {
- const mockStream = createMockStream(["Hello world", "STOP"])
- const stopTokens = ["STOP"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("Hello world")
- })
-
- it("should handle multiple stop tokens of different lengths", async () => {
- const mockStream = createMockStream(["This is a ", "test with ", "multiple STOP", " tokens END"])
- const stopTokens = ["STOP", "END", "HALT"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("This is a test with multiple ")
- })
-
- it("should handle an empty stream", async () => {
- const mockStream = createMockStream([])
- const stopTokens = ["STOP"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("")
- })
-
- it("should handle an empty stop tokens array", async () => {
- const mockStream = createMockStream(["Hello", " world!"])
- const stopTokens: string[] = []
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- const output = []
- for await (const char of result) {
- output.push(char)
- }
-
- expect(output.join("")).toBe("Hello world!")
- })
-
- it("should handle stop token when remaining buffer is smaller than maximum stop token length", async () => {
- const mockStream = createMockStream(["Hello world!STOP"])
- const stopTokens: string[] = ["STOP", "STOP_TOKEN_THAT_IS_LARGER_THAN_BUFFER"]
- const result = stopAtStopTokens(mockStream, stopTokens)
-
- expect(await streamToString(result)).toBe("Hello world!")
- })
-})
-
-describe("stopAtStartOf", () => {
- const sampleCode = ` {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: \`Bearer \${this.workOsAccessToken}\`,
- },
- },
- );
- const data = await response.json();
- return data.items;
- }
-
- async getContextItems(
- query: string,
- extras: ContextProviderExtras,
- ): Promise {
- const response = await extras.fetch(
- new URL(
- \`/proxy/context/\${this.options.id}/retrieve\`,
- controlPlaneEnv.CONTROL_PLANE_URL,
- ),
-`
-
- /* Some LLMs, such as Codestral, repeat the suffix of the query. To test our filtering, we cut the sample code at random positions, remove a part of the input
-and construct a response, containing the removed part and the suffix. The goal of the stopAtStartOf() method is to detect the start of the suffix in the response */
- it("should stop if the start of the suffix is reached", async () => {
- const suffix = `
- const data = await response.json();
- return data.items;
-}`
- const mockStream = createMockStream(sampleCode.split(/(?! )/g))
- const result = stopAtStartOf(mockStream, suffix)
-
- const resultStr = await streamToString(result)
- expect(resultStr).toBe(` {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: \`Bearer \${this.workOsAccessToken}\`,
- },
- },
- );
- `)
- })
- it("should stop if the start of the suffix is reached, even if the suffix has a prefix", async () => {
- const suffix = `
- xxxconst data = await response.json();
- return data.items;
-}`
- const mockStream = createMockStream(sampleCode.split(/(?! )/g))
- const result = stopAtStartOf(mockStream, suffix)
-
- const resultStr = await streamToString(result)
- expect(resultStr).toBe(` {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: \`Bearer \${this.workOsAccessToken}\`,
- },
- },
- );
- `)
- })
-})
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.ts
deleted file mode 100644
index d19bed609ca..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/charStream.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * Asynchronously yields characters from the input stream, stopping if a stop token is encountered.
- *
- * @param {AsyncGenerator} stream - The input stream of characters.
- * @param {string[]} stopTokens - Array of tokens that signal when to stop yielding.
- * @yields {string} Characters from the input stream.
- * @returns {AsyncGenerator} An async generator that yields characters until a stop condition is met.
- * @description
- * 1. If no stop tokens are provided, yields all characters from the stream.
- * 2. Otherwise, buffers incoming chunks and checks for stop tokens.
- * 3. Yields characters one by one if no stop token is found at the start of the buffer.
- * 4. Stops yielding and returns if a stop token is encountered.
- * 5. After the stream ends, filters encountered stop tokens in remaining buffer.
- * 6. Yields any remaining buffered characters.
- */
-export async function* stopAtStopTokens(stream: AsyncGenerator, stopTokens: string[]): AsyncGenerator {
- if (stopTokens.length === 0) {
- for await (const char of stream) {
- yield char
- }
- return
- }
-
- const maxStopTokenLength = Math.max(...stopTokens.map((token) => token.length))
- let buffer = ""
-
- for await (const chunk of stream) {
- buffer += chunk
-
- while (buffer.length >= maxStopTokenLength) {
- let found = false
- for (const stopToken of stopTokens) {
- if (buffer.startsWith(stopToken)) {
- found = true
- return
- }
- }
-
- if (!found) {
- yield buffer[0]
- buffer = buffer.slice(1)
- }
- }
- }
- // Filter out the possible stop tokens from remaining buffer
- stopTokens.forEach((token) => {
- buffer = buffer.replace(token, "")
- })
-
- // Yield any remaining characters in the buffer
- for (const char of buffer) {
- yield char
- }
-}
-
-/**
- * Asynchronously yields characters from the input stream.
- * Stops if the beginning of the suffix is detected in the stream.
- */
-export async function* stopAtStartOf(
- stream: AsyncGenerator,
- suffix: string,
- sequenceLength: number = 20,
-): AsyncGenerator {
- if (suffix.length < sequenceLength) {
- for await (const chunk of stream) {
- yield chunk
- }
- return
- }
- // We use sequenceLength * 1.5 as a heuristic to make sure we don't miss the sequence if the
- // stream is not perfectly aligned with the sequence (small whitespace differences etc).
- const targetPart = suffix.trimStart().slice(0, Math.floor(sequenceLength * 1.5))
-
- let buffer = ""
-
- for await (const chunk of stream) {
- buffer += chunk
-
- // Check if the targetPart contains contains the buffer at any point
- if (buffer.length >= sequenceLength && targetPart.includes(buffer)) {
- return // Stop processing when the sequence is found
- }
-
- // Yield chunk by chunk, ensuring not to exceed sequenceLength in the buffer
- while (buffer.length > sequenceLength) {
- yield buffer[0]
- buffer = buffer.slice(1)
- }
- }
-
- // Yield the remaining buffer if it is not contained in the `targetPart`
- if (buffer.length > 0) {
- yield buffer
- }
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/lineStream.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/lineStream.ts
index 2a0be704a67..a4cc5e15265 100644
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/lineStream.ts
+++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/streamTransforms/lineStream.ts
@@ -1,4 +1,4 @@
-import { LineStream } from "../../../diff/util"
+import type { LineStream } from "../../../diff/util"
import { lineIsRepeated } from "../../util/textSimilarity"
export { lineIsRepeated }
@@ -177,19 +177,6 @@ export async function* stopAtLines(
}
}
-/**
- * Yield until an exact stop line is encountered, then call fullStop.
- */
-export async function* stopAtLinesExact(stream: LineStream, fullStop: () => void, linesToStopAt: string[]): LineStream {
- for await (const line of stream) {
- if (linesToStopAt.some((stopAt) => line === stopAt)) {
- fullStop()
- break
- }
- yield line
- }
-}
-
/**
* On the first line only, strip any configured prefix (e.g. "").
*/
@@ -230,41 +217,3 @@ export async function* stopAtRepeatingLines(lines: LineStream, fullStop: () => v
previousLine = line
}
}
-
-/**
- * Pass through lines, but if the stream takes longer than ms after we have at least one non-empty line, stop early.
- */
-export async function* showWhateverWeHaveAtXMs(lines: LineStream, ms: number): LineStream {
- const startTime = Date.now()
- let firstNonWhitespaceLineYielded = false
-
- for await (const line of lines) {
- yield line
-
- if (!firstNonWhitespaceLineYielded && line.trim() !== "") {
- firstNonWhitespaceLineYielded = true
- }
-
- const isTakingTooLong = Date.now() - startTime > ms
- if (isTakingTooLong && firstNonWhitespaceLineYielded) {
- break
- }
- }
-}
-
-/**
- * Yield lines until the first blank line after some content; then stop.
- */
-export async function* noDoubleNewLine(lines: LineStream): LineStream {
- let isFirstLine = true
-
- for await (const line of lines) {
- if (line.trim() === "" && !isFirstLine) {
- return
- }
-
- isFirstLine = false
-
- yield line
- }
-}
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_JSON.txt b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_JSON.txt
deleted file mode 100644
index ff0ba3cb39a..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_JSON.txt
+++ /dev/null
@@ -1,60 +0,0 @@
-##### Prompt #####
-{
- "active": true,
- "department": "Product Development",
- "location": {
- "country": "USA",
- "state": "California",
- "city": "San BERNARDINO",
- "coordinates": {
-
- }
- },
- "employees": [
- {
- "name": "John Doe",
- "age": 30,
- "position": "Developer",
- "skills": ["JavaScript", "React", "Node.js"],
- "remote": false,
- "salary": {
- "currency": "USD",
- "amount": 95000
- }
- },
- {
- "name": "Jane Smith",
- "age": 25,
- "position": "Designer",
- "skills": ["Photoshop", "Illustrator"],
- "remote": true,
- "salary": {
- "currency": "USD",
- "amount": 70000
- }
- },
- {
- "name": "Emily Johnson",
- "age": 35,
- "position": "Manager",
- "teamSize": 8,
- "remote": true,
- "skills": ["Leadership", "Project Management"],==========================================================================
-==========================================================================
-Completion:
-
- "latitude": 34.10834,
- "longitude": -117.28977
- }
- },
- "employeeCount": 2,
- "averageAge": 30,
- "remoteFriendly": true,
- "salaryRange": {
- "min": 70000,
- "max": 95000,
- "currency": "USD"
- },
- "skills": {
- "required": ["JavaScript", "React", "Node.js", "Leadership", "Project Management"],
- "optional": ["Photoshop", "Illustrator"]
\ No newline at end of file
diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_TYPESCRIPT.txt b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_TYPESCRIPT.txt
deleted file mode 100644
index 241d141f28b..00000000000
--- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/filtering/test/NEGATIVE_TEST_CASES/QWEN_TYPESCRIPT.txt
+++ /dev/null
@@ -1,128 +0,0 @@
-##### Prompt #####
- }`,
- },
- {
- description: "Should autocomplete Vue computed property",
- filename: "UserComponent.vue",
- input: `
-
- );
-};`,
- llmOutput: "0",
- expectedCompletion: "0",
- filename: "Counter.tsx",
- },
- {
- description: "Should autocomplete React component methods",
- input: `import React from "react";
-
-class Form extends React.Component {
- constructor(props) {
- super(props);
- this.state = { name: '' };
- }
-
- handleChange = (event) => {
- <|fim|>
- }
-
- handleSubmit = (event) => {
- event.preventDefault();
- alert('A name was submitted: ' + this.state.name);
- }
-
- render() {
- return (
-
- );
- }
-}`,
- llmOutput: "this.setState({ name: event.target.value });",
- expectedCompletion: "this.setState({ name: event.target.value });",
- filename: "Form.tsx",
- },
- {
- description: "Should autocomplete Python function definition",
- filename: "test.py",
- input: `def calculate_area(length, width):
- area = length * width
- return area
-
-def calculate_perimeter(length, width):
- <|fim|>
-`,
- llmOutput: `perimeter = 2 * (length + width)
- return perimeter`,
- expectedCompletion: `perimeter = 2 * (length + width)
- return perimeter`,
- },
- {
- description: "Should complete Python class method with self",
- filename: "test.py",
- input: `class BankAccount:
- def __init__(self, owner, balance=0):
- self.owner = owner
- self.balance = balance
-
- def deposit(self, amount):
- self.balance += amount
- return self.balance
-
- def withdraw(self, amount):
- <|fim|>
-`,
- llmOutput: `if amount > self.balance:
- return "Insufficient funds"
- self.balance -= amount
- return self.balance`,
- expectedCompletion: `if amount > self.balance:
- return "Insufficient funds"
- self.balance -= amount
- return self.balance`,
- },
- {
- description: "Should autocomplete Python list comprehension",
- filename: "test.py",
- input: `squares = [x**2 for x in range(10)]
-even_squares = [x for x in squares if x % 2 == 0]
-print(even_squares)
-
-odd_squares = [<|fim|>
-print(odd_squares)
-`,
- llmOutput: "x for x in squares if x % 2 != 0",
- expectedCompletion: "x for x in squares if x % 2 != 0",
- },
- {
- description: "Should autocomplete a simple Go function declaration",
- filename: "simpleFunction.go",
- input: `package main
-
-import (
- "fmt"
-)
-
-func main() {
- fmt.Println("Hello, World!")
-}
-
-func calculateArea<|fim|>`,
- llmOutput: `(length float64, width float64) float64 {
- return length * width
-}`,
- expectedCompletion: `(length float64, width float64) float64 {
- return length * width
-}`,
- },
-
- {
- description: "Should handle autocomplete in the middle of a Go struct definition",
- filename: "structDefinition.go",
- input: `package main
-
-type Person struct {
- FirstName string
- LastName string
- Age int
- Address Address
-}
-
-type Address struct {
- Street string
- City <|fim|>
-}
-
-func main() {}`,
- llmOutput: `string
- State string
- ZipCode string
-}`,
- expectedCompletion: `string
- State string
- ZipCode string`,
- },
-
- {
- description: "Should autocomplete a missing Go function body bracket",
- filename: "missingBracket.go",
- input: `package main
-
-func add(a int, b int) int {
- return a + b<|fim|>
-
-func multiply(a int, b int) int {
- return a * b
-}
-
-func main() {
- result1 := add(2, 3)
- result2 := multiply(4, 5)
- println(result1, result2)
-}`,
- llmOutput: `
-}`,
- expectedCompletion: `
-}`,
- },
- {
- description: "Should autocomplete SQL query with nested functions and missing bracket",
- filename: "_nested_function.sql",
- input: `SELECT name, ROUND(AVG(rating), 2) as avg_rating
- FROM movies
- WHERE release_year > 2000 AND director = 'Christopher Nolan'
- GROUP BY name
- HAVING avg_rating > <|fim|>`,
- llmOutput: "8.5)",
- expectedCompletion: "8.5)",
- },
- {
- description: "Should autocomplete SQL script with employee and product tables",
- filename: "database.sql",
- input: `
- CREATE TABLE employees (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(100) NOT NULL,
- age INT NOT NULL,
- position VARCHAR(100)
- );
-
- CREATE TABLE products (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(100),
- price DECIMAL(8,2) NOT NULL<|fim|> '0.00',
- quantity INT NOT NULL DEFAULT '0'
- );
-
- INSERT INTO employees (name, age, position) VALUES ('John Doe', 30, 'Developer');
- INSERT INTO products (name, price, quantity) VALUES ('Apple', '1.99', '47');
-
- SELECT * FROM products ORDER BY name DESC LIMIT 3;
- SELECT * FROM products WHERE price > '0';
- SELECT * FROM products WHERE quantity > '100';
- SELECT * FROM employees WHERE age > 25;
- `,
- llmOutput: " DEFAULT",
- expectedCompletion: " DEFAULT",
- },
- {
- description: "Should autocomplete multi-line SQL query with CASE statements",
- filename: "case_statement.sql",
- input: `SELECT order_id,
- order_date,
- CASE
- WHEN status = 'shipped' THEN 'Completed'
- WHEN status = 'pending' THEN 'Pending Approval'
- <|fim|>
- ELSE 'Unknown'
- END as order_status
- FROM orders`,
- llmOutput: "WHEN status = 'cancelled' THEN 'Cancelled'",
- expectedCompletion: "WHEN status = 'cancelled' THEN 'Cancelled'",
- },
- {
- description: "Should autocomplete HTML paragraph content",
- input: `
-
- Document
-
-
-
<|fim|>
-
-`,
- llmOutput: "This is a paragraph with some sample text.",
- expectedCompletion: "This is a paragraph with some sample text.",
- filename: "test.html",
- },
- {
- description: "Should autocomplete HTML attributes within a tag",
- input: `
- >
-
-
Title
-
Description text.
-
-
`,
- llmOutput: 'alt="Description of image"',
- expectedCompletion: 'alt="Description of image"',
- filename: "test.html",
- },
- {
- description: "Should autocomplete HTML nested tags",
- input: `
-
Item 1
-
Item 2
-
Item 3
-
`,
- llmOutput: ">Item 4",
- expectedCompletion: ">Item 4",
- filename: "test.html",
- },
-
- {
- description: "Should complete a class method in Ruby",
- input: `
-class Greeter
- def initialize(name)
- @name = name
- end
-
- def greet
- puts "Hello, <|fim|>
- end
-end
-
-g = Greeter.new("World")
-g.greet
-`,
- llmOutput: "#{@name}!",
- expectedCompletion: "#{@name}!",
- filename: "greeter.rb",
- },
- {
- description: "Should complete Ruby if-else block",
- input: `
-number = 10
-
-if number > 5
- puts "Number is greater than 5"
-<|fim|>
-end
-`,
- llmOutput: `else
- puts "Number is 5 or less"`,
- expectedCompletion: `else
- puts "Number is 5 or less"`,
- filename: "conditional.rb",
- },
- {
- description: "Should complete Ruby array method",
- input: `
-numbers = [1, 2, 3, 4, 5]
-squared_numbers = numbers.<|fim|>
-`,
- llmOutput: "map { |n| n ** 2 }",
- expectedCompletion: "map { |n| n ** 2 }",
- filename: "array_methods.rb",
- },
- {
- description: "Should autocomplete Java within a string",
- filename: "App.java",
- input: `
-public class App {
- public static void main(String[] args) {
- String message = "Hello, <|fim|>";
- System.out.println(message);
- }
-}`,
- llmOutput: "World",
- expectedCompletion: "World",
- },
- {
- description: "Should autocomplete a C++ class definition with a constructor",
- filename: "test.cpp",
- input: "class Vehicle { public: Vehicle(<|fim|>); };",
- llmOutput: "int wheels, double weight",
- expectedCompletion: "int wheels, double weight",
- },
- {
- description: "Should complete a C++ method declaration inside a class",
- filename: "test.cpp",
- input: "class Calculator { public: int add(int a, int b); int subtract(int a, int b);<|fim|> };",
- llmOutput: " int multiply(int a, int b);",
- expectedCompletion: " int multiply(int a, int b);",
- },
- {
- description: "Should autocomplete C++ for loop syntax",
- filename: "test.cpp",
- input: `
- int sum = 0;
- for (int i = 0; i < 10; <|fim|>) {
- sum += i;
- }
- `,
- llmOutput: "i++",
- expectedCompletion: "i++",
- },
- {
- description: "Should autocomplete JSON object inside an array",
- filename: "data.json",
- input: `{
- "users": [
- { "id": 1, "name": "Alice" },
- { "id": 2, "name": "Bob" }
- <|fim|>
- ]
-}`,
- llmOutput: ', { "id": 3, "name": "Charlie" }',
- expectedCompletion: ', { "id": 3, "name": "Charlie" }',
- },
- {
- description: "Should autocomplete within a CSV record",
- filename: "test.csv",
- input: `Name, Age, City
-John Doe, 30, New York
-Jane Smith<|fim|>`,
- llmOutput: ", 25, Los Angeles",
- expectedCompletion: ", 25, Los Angeles",
- },
- {
- description: "Should complete CSV record when starting in the middle of a word",
- filename: "test.csv",
- input: `Product, Price, Quantity
-Laptop, 1200, 5
-Smart<|fim|>`,
- llmOutput: "phone, 800, 10",
- expectedCompletion: "phone, 800, 10",
- },
- {
- description: "Should complete CSV structure adding closing brackets",
- filename: "test.csv",
- input: `ID, Name, JoiningDate
-1, Alice, 2023-01-10
-2, B<|fim|>`,
- llmOutput: "ob, 2023-02-10",
- expectedCompletion: "ob, 2023-02-10",
- },
- {
- description: "Should autocomplete a Rust function implementation inside a struct",
- filename: "main.rs",
- input: `
-struct Calculator {
- result: f64,
-}
-
-impl Calculator {
- fn new() -> Self {
- Calculator { result: 0.0 }
- }
-
- fn add(&mut self, number: f64) {
- self.result += number;
- }
-
- fn subtract(&mut self, number: f64) {
- self.result -= number;
- }
-
- fn multiply(&mut self, number: f64) {
- self.result *= number;
- }
-
- fn divide(&mut self, number: f64) {
- if number != 0.0 {
- self.result /= number;
- } else {
- println!("Cannot divide by zero.");
- }
- }
-
- fn reset(&mut self) {
- self.result = 0.0;
- }
-
- fn get_result(&self) -> f64 {
- self.result
- }
-
- fn<|fim|>
-}
-`,
- llmOutput: ` divide(&mut self, number: f64) {
- if number != 0.0 {
- self.result /= number;
- } else {
- println!("Cannot divide by zero.");
- }
-}`,
- expectedCompletion: ` divide(&mut self, number: f64) {
- if number != 0.0 {
- self.result /= number;
- } else {
- println!("Cannot divide by zero.");
- }`,
- },
- {
- description: "Should autocomplete Rust struct definition",
- filename: "main.rs",
- input: `
-struct User {
- id: u32,
- username: String,
- email: String,
- is_active: bool,
- <|fim|>
-}
-
-impl User {
- fn new(id: u32, username: String, email: String) -> Self {
- User {
- id,
- username,
- email,
- is_active: true,
- }
- }
-}
-`,
- llmOutput: `created_at: String,
- updated_at: String,`,
- expectedCompletion: `created_at: String,
- updated_at: String,`,
- },
- {
- description: "Haskell: Nested pattern matching with let bindings",
- filename: "NestedPattern.hs",
- input: `module NestedPattern where
-
-data Tree a = Leaf a | Node (Tree a) (Tree a)
-
-sumTree :: Num a => Tree a -> a
-sumTree (Leaf x) = x
-sumTree (Node left right) =
- let leftSum = <|fim|>
- rightSum = sumTree right
- in leftSum + rightSum`,
- llmOutput: "sumTree left",
- expectedCompletion: "sumTree left",
- },
- {
- description: "Haskell: Complex function with where clause and guards",
- filename: "QuadraticSolver.hs",
- input: `module QuadraticSolver where
-
-solveQuadratic :: (Ord a, Floating a) => a -> a -> a -> Maybe (a, a)
-solveQuadratic a b c
- | discriminant < 0 = Nothing
- | otherwise = Just (x1, x2)
- where
- discriminant = b^2 - 4*a*c
- sqrtD = sqrt discriminant
- x1 = (-b + sqrtD) / (2*a)
- <|fim|> = (-b - sqrtD) / (2*a)`,
- llmOutput: "x2",
- expectedCompletion: "x2",
- },
- {
- description: "Haskell: List comprehension with complex filter",
- filename: "PrimeNumbers.hs",
- input: `module PrimeNumbers where
-
-primesUpTo :: Int -> [Int]
-primesUpTo n = [x | x <- [2..n], isPrime x]
- where isPrime num = <|fim|> && all (\\d -> num \`mod\` d /= 0) [2..(floor . sqrt $ fromIntegral num)]`,
- llmOutput: "num > 1",
- expectedCompletion: "num > 1",
- },
- {
- description: "Should autocomplete Dart class methods",
- filename: "calculator.dart",
- input: `
-class Calculator {
- double result = 0.0;
-
- void add(double number) {
- result += number;
- }
-
- void multiply(double number) {
- result *= number;
- }
-
- <|fim|>
-
- double getResult() {
- return result;
- }
-}`,
- llmOutput: `void subtract(double number) {
- result -= number;
- }`,
- expectedCompletion: `void subtract(double number) {
- result -= number;
- }`,
- },
- {
- description: "Should handle string interpolation in Dart",
- filename: "greetings.dart",
- input: `
-void main() {
- var name = "World";
- print('Hello, <|fim|>!');
-}`,
- llmOutput: "${name}",
- expectedCompletion: "${name}",
- },
- {
- description: "Should autocomplete within a Dart function body",
- filename: "counter.dart",
- input: `
-class Counter {
- int count = 0;
-
- void increment() {
- count++;
- }
-
- void decrement() {
- <|fim|>
-
- void reset() {
- count = 0;
- }
-}`,
- llmOutput: "count--;",
- expectedCompletion: "count--;",
- },
- {
- description: "Should autocomplete Clojure function definition with missing closing parenthesis",
- input: `(defn calculate-sum [a b]
- (let [sum (+ a b)]
- (println "The sum is" sum)
- sum<|fim|>`,
- llmOutput: "))",
- expectedCompletion: "))",
- filename: "test.clj",
- },
- {
- description: "Should autocomplete missing part of a Clojure map within a function",
- input: `(defn get-user []
- {:username "johndoe"
- :email "johndoe@example.com"
- :age 30
- <|fim|>
- (println "User information loaded"))`,
- llmOutput: ':location "Unknown"}',
- expectedCompletion: ':location "Unknown"}',
- filename: "test.clj",
- },
- {
- description: "Should autocomplete inside a Clojure vector within a looping construct",
- input: `(defn odd-numbers []
- (loop [nums [1 3 5<|fim|> 9 11]]
- (when (seq nums)
- (println (first nums))
- (recur (rest nums)))))`,
- llmOutput: " 7,",
- expectedCompletion: " 7,",
- filename: "test.clj",
- },
- {
- description: "Should autocomplete R function definition",
- filename: "calculate.R",
- input: `
-calculate_mean <- function(numbers) {
- total <- sum(numbers)
- <|fim|>
-}`,
- llmOutput: `mean_value <- total / length(numbers)
- return(mean_value)`,
- expectedCompletion: `mean_value <- total / length(numbers)
- return(mean_value)`,
- },
- {
- description: "Should complete R loop and print statement",
- filename: "loopPrint.R",
- input: `
-numbers <- c(1, 2, 3, 4, 5)
-for (number in numbers) {
- print(<|fim|>)
-}`,
- llmOutput: "number)",
- expectedCompletion: "number)",
- },
- {
- description: "Should autocomplete R data frame creation",
- filename: "dataFrame.R",
- input: `
-data <- data.frame(
- Name = c("Alice", "Bob", "Charlie"),
- Age = c(25, 30, 35),
- <|fim|>
-)`,
- llmOutput: "Height = c(165, 180, 175)",
- expectedCompletion: "Height = c(165, 180, 175)",
- },
- {
- description: "Should autocomplete R if-else statement",
- filename: "condition.R",
- input: `
-grade <- 85
-if (grade >= 90) {
- print("A")
-} else if (grade >= 80) {
- <|fim|>
-} else {
- print("C")
-}`,
- llmOutput: 'print("B")',
- expectedCompletion: 'print("B")',
- },
- {
- description: "Should autocomplete R ggplot2 plot structure",
- filename: "plot.R",
- input: `
-library(ggplot2)
-
-ggplot(data=mtcars, aes(x=wt, y=mpg)) +
- geom_point() +
- <|fim|>`,
- llmOutput: "geom_smooth(method='lm', se=FALSE)",
- expectedCompletion: "geom_smooth(method='lm', se=FALSE)",
- },
- {
- description: "Should autocomplete Scala class with a method",
- filename: "Person.scala",
- input: `class Person(val name: String, val age: Int) {
- def greet(): String = {
- <|fim|>
- }
- }`,
- llmOutput: 's"Hello, my name is $name and I am $age years old."',
- expectedCompletion: 's"Hello, my name is $name and I am $age years old."',
- },
-
- {
- description: "Should handle Scala case class with a missing field",
- filename: "Person.scala",
- input: `case class Address(city: String, postalCode: String)
- case class Person(name: String, age: Int, address: Address)
-
- val alice = Person("Alice", 30, Address("Wonderland", <|fim|>))`,
- llmOutput: '"12345")',
- expectedCompletion: '"12345")',
- },
-
- {
- description: "Should autocomplete Scala function with missing body bracket",
- filename: "Math.scala",
- input: `object MathUtils {
- def add(a: Int, b: Int): Int = {
- a + b<|fim|>
-
- def multiply(a: Int, b: Int): Int = {
- a * b
- }
- }
-
- object Main extends App {
- println(MathUtils.add(3, 5))
- println(MathUtils.multiply(4, 6))
- }`,
- llmOutput: `
- }`,
- expectedCompletion: `
- }`,
- },
- {
- description: "Should autocomplete C function definition",
- filename: "math_utils.c",
- input: `#include
-
-int add(int a, int b) {
- return a + b;
-}
-
-int multiply(int a, int b) {
- return a * b;
-}
-
-int subtract(int a, int b) {
- <|fim|>
-}
-
-int main() {
- printf("Result: %d", add(2, 3));
- return 0;
-}`,
- llmOutput: "return a - b;",
- expectedCompletion: "return a - b;",
- },
-
- {
- description: "Should handle C struct with missing field initialization",
- filename: "person.c",
- input: `#include
-
-typedef struct {
- char name[50];
- int age;
- float height;
-} Person;
-
-int main() {
- Person alice = {"Alice", 30, <|fim|>};
- printf("Name: %s, Age: %d, Height: %.2f", alice.name, alice.age, alice.height);
- return 0;
-}`,
- llmOutput: "5.5",
- expectedCompletion: "5.5",
- },
-
- {
- description: "Should autocomplete C function with missing body bracket",
- filename: "area.c",
- input: `#include
-
-double calculateCircleArea(double radius) {
- const double pi = 3.14159;
- return pi * radius * radius;<|fim|>
-
-double calculateRectangleArea(double length, double width) {
- return length * width;
-}
-
-int main() {
- printf("Circle Area: %.2f", calculateCircleArea(5.0));
- printf("Rectangle Area: %.2f", calculateRectangleArea(4.0, 6.0));
- return 0;
-}`,
- llmOutput: `
-}`,
- expectedCompletion: `
-}`,
- },
- {
- description: "Should autocomplete a simple Kotlin function declaration",
- filename: "simpleFunction.kt",
- input: `
-fun main() {
- println("Hello, World!")
-}
-
-fun calculateArea(length: Double, width: Double): Double <|fim|>`,
- llmOutput: `{
- return length * width
-}`,
- expectedCompletion: `{
- return length * width
-}`,
- },
- {
- description: "Should handle autocomplete inside a Kotlin data class",
- filename: "dataClass.kt",
- input: `
-data class User(
- val id: Int,
- val name: String,
- val email: String,
- <|fim|>
-)`,
- llmOutput: "val age: Int",
- expectedCompletion: "val age: Int",
- },
- {
- description: "Should complete Kotlin if-else structure with missing brackets",
- filename: "controlStructure.kt",
- input: `
-fun getMax(a: Int, b: Int): Int {
- if (a > b<|fim|>
- } else {
- return b
- }
-}`,
- llmOutput: `) {
- return a`,
- expectedCompletion: `) {
- return a`,
- },
- {
- description: "Should autocomplete Solidity function definition",
- filename: "SimpleStorage.sol",
- input: `
-pragma solidity ^0.8.0;
-
-contract SimpleStorage {
- uint private data;
-
- function set(uint x) public {
- data = x;
- }
-
- function get() public view returns (uint) {
- <|fim|>
- }
-}
- `,
- llmOutput: "return data;",
- expectedCompletion: "return data;",
- },
-
- {
- description: "Should autocomplete Solidity event with parameters",
- filename: "EventExample.sol",
- input: `
-pragma solidity ^0.8.0;
-
-contract EventExample {
- event DataStored(uint indexed id, string content);
-
- function storeData(uint id, string memory content) public {
- emit DataStored(<|fim|>);
- }
-}
- `,
- llmOutput: "id, content",
- expectedCompletion: "id, content",
- },
-
- {
- description: "Should handle Solidity struct definition completion",
- filename: "StructDefinition.sol",
- input: `
-pragma solidity ^0.8.0;
-
-contract StructExample {
- struct Person {
- string name;
- uint age;
- address wallet;
- }
-
- Person[] private people;
-
- function addPerson(string memory name, uint age, address wallet) public {
- people.push(Person(name, age, wallet));
- }
-
- function getFirstPerson<|fim|>
-}
- `,
- llmOutput: `() public view returns (string memory, uint, address) {
- if (people.length > 0) {
- Person storage person = people[0];
- return (person.name, person.age, person.wallet);
- }
- return ("", 0, address(0));
- }`,
- expectedCompletion: `() public view returns (string memory, uint, address) {
- if (people.length > 0) {
- Person storage person = people[0];
- return (person.name, person.age, person.wallet);
- }
- return ("", 0, address(0));
- }`,
- },
- {
- description: "Should autocomplete TypeScript interface properties",
- filename: "User.ts",
- input: `
-interface User {
- id: number;
- name: string;
- e<|fim|>
-}
-`,
- llmOutput: `mail: string;
- age: number;
-}`,
- expectedCompletion: `mail: string;
- age: number;`,
- },
- {
- description: "Should autocomplete TypeScript interface declarations",
- filename: "autocomplete.ts",
- input: `interface AutocompleteDiffSnippet extends BaseAutocompleteSnippet {}
-
-interface AutocompleteCodeSnippet`,
- llmOutput: ` extends BaseAutocompleteSnippet {
- filepath: string;
-}`,
- expectedCompletion: ` extends BaseAutocompleteSnippet {
- filepath: string;
-}`,
- },
- {
- description: "Should autocomplete a TypeScript arrow function inside a variable assignment",
- filename: "mathOperations.ts",
- input: `
-const addNumbers = (a: number, b: number): number => {
- return a + b;
-};
-
-const multiplyNumbers = (a: number, b: number): number => {
- re<|fim|>
-}
-
-console.log(multiplyNumbers(2, 3));
-`,
- llmOutput: "turn a * b;",
- expectedCompletion: "turn a * b;",
- },
-
- // TODO
- // {
- // description:
- // "Should handle autocomplete inside a nested TypeScript class method",
- // filename: "Account.ts",
- // input: `
- // class Account {
- // private balance: number = 0;
-
- // deposit(amount: number) {
- // this.balance += amount;
- // return this.balance;
- // }
-
- // withdraw(amount: number) {
- // if (amount > this.balance) {
- // throw new Error("Insufficient funds");
- // }
- // this.balance -= amount;
- // return thi<|fim|>
- // }
- // }
- // `,
- // llmOutput: `s.balance;`,
- // expectedCompletion: `s.balance;`,
- // },
-
- // TODO
- // {
- // description: "Should autocomplete a TypeScript generic function",
- // filename: "GenericFunction.ts",
- // input: `
- // function identity(arg: T): T {
- // return ar<|fim|>
- // }
-
- // console.log(identity(5));
- // `,
- // llmOutput: `g;`,
- // expectedCompletion: `g;`,
- // },
-
- // TODO
- // {
- // description:
- // "Should autocomplete a TypeScript promise within an asynchronous function",
- // filename: "asyncFunction.ts",
- // input: `
- // async function fetchData(url: string): Promise {
- // const response = await fetch(url);
- // <|fim|>
- // return data;
- // }
-
- // fetchData('https://api.example.com/data');
- // `,
- // llmOutput: `const data = await response.json();`,
- // expectedCompletion: `const data = await response.json();`,
- // },
-
- {
- description: "Should autocomplete a C# class with a constructor and property",
- filename: "Person.cs",
- input: `using System;
-
-public class Person
-{
- public string Name { get; set; }
- public int Age { get; set; }
-
- public Person(string name, int <|fim|>
-}`,
- llmOutput: `age)
- {
- Name = name;
- Age = age;
- }`,
- expectedCompletion: `age)
- {
- Name = name;
- Age = age;
- }`,
- },
- {
- description: "Should autocomplete a C# interface method",
- filename: "IGreetable.cs",
- input: `public interface IGreetable
-{
- void <|fim|>
-}`,
- llmOutput: "Greet();",
- expectedCompletion: "Greet();",
- },
- {
- description: "Should autocomplete inside C# method with if condition",
- filename: "Calculator.cs",
- input: `using System;
-
-public class Calculator
-{
- public int Add(int a, int b)
- {
- if(<|fim|>)
- {
- return a + b;
- }
- return 0;
- }
-}`,
- llmOutput: "a > 0 && b > 0",
- expectedCompletion: "a > 0 && b > 0",
- },
- {
- description: "Should complete a simple Julia function",
- filename: "simpleFunction.jl",
- input: `function calculate_area(length, width)
- <|fim|>
-end
-`,
- llmOutput: "return length * width",
- expectedCompletion: "return length * width",
- },
- {
- description: "Should autocomplete Julia for loop",
- filename: "loop.jl",
- input: `numbers = [1, 2, 3, 4, 5]
-squared_numbers = []
-
-for num in numbers
- <|fim|>
-end
-
-println(squared_numbers)
-`,
- llmOutput: "push!(squared_numbers, num^2)",
- expectedCompletion: "push!(squared_numbers, num^2)",
- },
- {
- description: "Should complete a Julia struct definition",
- filename: "structDefinition.jl",
- input: `struct Person
- first_name::String
- last_name::String
- age::Int
- address::Address
-end
-
-struct Address
- street::String
- city::String
- <|fim|>
-end
-`,
- llmOutput: `state::String
- zip_code::String`,
- expectedCompletion: `state::String
- zip_code::String`,
- },
- {
- description: "Should complete a Julia dictionary access",
- filename: "dictionary.jl",
- input: `grades = Dict("Alice" => 90, "Bob" => 85, "Eve" => 88)
-
-function get_grade(student_name)
- return grades[<|fim|>]
-end
-
-println(get_grade("Alice")) # Should print 90
-`,
- llmOutput: "student_name",
- expectedCompletion: "student_name",
- },
- {
- description: "Should complete a Julia module declaration",
- filename: "moduleDeclaration.jl",
- input: `module MathOperations
-
-export add, subtract
-
-function add(a, b)
- return a + b
-end
-
-function subtract(a, b)
- return a - b
-end
-
-<|fim|>
-`,
- llmOutput: "end",
- expectedCompletion: "end",
- },
- {
- description: "Should complete F# let-binding with function definition",
- filename: "mathModule.fs",
- input: `module MathModule
-
-let calculateArea length width =
- <|fim|>`,
- llmOutput: "length * width",
- expectedCompletion: "length * width",
- },
-
- {
- description: "Should complete incomplete F# type definition",
- filename: "personType.fs",
- input: `type Person = {
- FirstName: string
- LastName: string
- Age: int<|fim|>
-}
-
-let john = { FirstName = "John"; LastName = "Doe"; Age = 30 }`,
- llmOutput: `
- Address: string
-}`,
- expectedCompletion: `
- Address: string`,
- },
-
- {
- description: "Should complete F# pattern matching expression",
- filename: "patternMatching.fs",
- input: `let describeNumber number =
- match number with
- | 0 -> "Zero"
- | 1 -> "One"
- | <|fim|>`,
- llmOutput: `2 -> "Two"
- | _ -> "Other"`,
- expectedCompletion: `2 -> "Two"
- | _ -> "Other"`,
- },
-
- {
- description: "Should complete F# list comprehension expression",
- filename: "listComprehension.fs",
- input: `let squares = [ for x in 1..10 -> x * x ]
-let evenSquares = [ for x in squares do if x % 2 = 0 then yield x ]
-let oddSquares = [<|fim|>]`,
- llmOutput: " for x in squares do if x % 2 <> 0 then yield x ]",
- expectedCompletion: " for x in squares do if x % 2 <> 0 then yield x ]",
- },
- {
- description: "Should complete an F# recursive function",
- filename: "recursiveFunctions.fs",
- input: `let rec factorial n =
- if n <= 1 then 1
- else n <|fim|> factorial (n - 1)`,
- llmOutput: "*",
- expectedCompletion: "*",
- },
- {
- description: "Should complete F# member method inside a class type",
- filename: "bankAccount.fs",
- input: `type BankAccount(owner: string, initialBalance: float) =
- let mutable balance = initialBalance
-
- member this.Deposit amount =
- balance <- balance + amount
- this
- <|fim|>`,
- llmOutput: `
- member this.Withdraw amount =
- if amount > balance then
- failwith "Insufficient funds"
- balance <- balance - amount
- this`,
- expectedCompletion: `
- member this.Withdraw amount =
- if amount > balance then
- failwith "Insufficient funds"
- balance <- balance - amount
- this`,
- },
-
- {
- description: "Should complete F# async workflow function",
- filename: "asyncWorkflow.fs",
- input: `let fetchDataAsync url =
- async {
- use client = new System.Net.Http.HttpClient()
- <|fim|>
- }`,
- llmOutput: `let! response = client.GetStringAsync(url)
- return response`,
- expectedCompletion: `let! response = client.GetStringAsync(url)
- return response`,
- },
- {
- description: "Should autocomplete SCSS nested class starting inside a rule",
- filename: "styles.scss",
- input: `nav {
- display: flex;
- justify-content: space-between;
- .logo {
- font-size: 1.5rem;
- color: #333;
-
- .brand<|fim|>
- }
-
- ul {
- list-style: none;
- display: flex;
- gap: 1rem;
- }
-}`,
- llmOutput: `-name {
- font-weight: bold;
- text-transform: uppercase;
-}`,
- expectedCompletion: `-name {
- font-weight: bold;
- text-transform: uppercase;
-}`,
- },
-
- {
- description: "Should handle SCSS mixin within a class",
- filename: "styles.scss",
- input: `.card {
- border: 1px solid #ccc;
- padding: 10px;
-
- @include<|fim|>
-
- &:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- }
-}`,
- llmOutput: " transition(all 0.3s ease);",
- expectedCompletion: " transition(all 0.3s ease);",
- },
-
- {
- description: "Should autocomplete SCSS variable in the middle of a statement",
- filename: "styles.scss",
- input: `$primary-color: #007bff;
-$secondary-color: #6c757d;
-
-.button {
- background-color: <|fim|> color;
- padding: 10px 15px;
- border: none;
- color: #fff;
- border-radius: 4px;
-}`,
- llmOutput: "$primary-",
- expectedCompletion: "$primary-",
- },
- {
- description: "Should autocomplete Vue component method",
- filename: "MyComponent.vue",
- input: `
-