Compare commits

...

37 Commits

Author SHA1 Message Date
Andrei Edell fe9265c63a Fix: Display original data:image URLs in rich display mode to maintain transparency 2025-04-17 13:41:23 -10:00
Andrei Edell 8d3981c618 Merge branch 'main' into pr-2146 2025-04-17 13:32:27 -10:00
canvrno fb3105f7bf ENG-470/Chunking for large terminal outputs (#2935)
* initial terminal output chunking

* changeset

* Update src/core/task/index.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* cleanup

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-17 16:22:27 -07:00
Saoud Rizwan bc87fdb4b3 fix: BrowserSessionRow to include 'reasoning' message type handling (#2953) 2025-04-17 12:58:10 -10:00
Trevor Hudson c021b6464b Trevor/eng 416 add editing ability to older user messages in chat (#2954)
* User message editing

* restore and send

* dont redo if the message is the same

* select by default

* resolve conflicts

* handle workspace restore

* add title to buttons

* don't allow restoring files if there is no workspace

* fix

* fix messaging

* fix text

* fix type
2025-04-17 15:38:30 -07:00
Evan 180ebdad74 Add edit cline rule button (#2956)
* add button to open rule file

* changeset
2025-04-17 13:33:11 -07:00
Andrei Edell 2a80fedf7d proto migration for testBrowserConnection.ts (#2922)
* proto migration for testBrowserConnection.ts

* format fix

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-17 09:31:48 -10:00
Evan 450583c81d à la mode (#2912)
* add fetching global cline rules files

* add toggle functionality to clinerules

* add toggles modal

* changeset

* change codicon

* fix bad merged files

* fix duplicate globalClineRulesToggles declaration in state.ts from merge

* refresh cline rules on modal open
2025-04-16 19:58:19 -07:00
Yaroslav Halchenko 45b1666325 Add codespell support (config, workflow to detect/not fix) and make it fix some typos (#2939)
* Add github action to codespell main on push and PRs

* Add rudimentary codespell config

* run codespell throughout fixing typos automagically (but ignoring overall fail due to ambigous ones)

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w || :",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^

* Do interactive fixing of some ambigous typos

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w -i 3 -C 4",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^

* Fix Formatting

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-16 19:53:16 -07:00
Matt Rubens 6c5b99d304 Safer check for xAI reasoning content (#2936) 2025-04-16 18:45:07 -07:00
Ara 02120810ce Adding o3 and o4-mini models (#2932)
* Revert "Add OpenAI o3 & 4o-mini (#2927)"

This reverts commit 89cbbe95e3.

* Adding O3 and O4-mini Models

* Adding O3 and O4-mini Models

* Adding O3 and O4-mini Models
2025-04-16 17:27:12 -07:00
Frostbourne 612744394a [ENG-417] Add command to focus chat input (#2910)
* Make command to focus on chat input

* Allow cmd to focus from anywhere

* changeset

* fix unit test

* Jump to chat input from anywhere

* fix focusChatInput call after opening ext
2025-04-16 16:00:10 -07:00
Trevor Hudson 01a48736eb Add ability to send context with an options selection (#2379)
* - add ability to send context with an options selection
- add sourcemaps for debugging in the webview

* remove colon if there is no message

* resolve conflicts

* remove sourcemap
2025-04-16 15:40:30 -07:00
Evan 4add38032e Add Accurate Title (#2934)
* Add openrouter ranking

* changeset
2025-04-16 12:55:17 -07:00
Evan 73078d63ce Pirate Mode Activated (#2890)
* add fetching global cline rules files

* add toggle functionality to clinerules

* selectively filter out OS generated files from read directory

* remove .file filtering

* remove duplicate imports

* pass path to global rules directory in system prompt

* empty commit to trigger tests
2025-04-16 12:44:04 -07:00
Peter Dave Hello 89cbbe95e3 Add OpenAI o3 & 4o-mini (#2927)
Reference:
- https://platform.openai.com/docs/models/o3
- https://platform.openai.com/docs/models/o4-mini
2025-04-16 11:16:55 -07:00
Andrei Edell 4d696f377c PROTOBUS: gRPC over vscode message passing (#2830)
* initial protobuf setup & rough domains

* delete old protos for now

* phase 1

* initial working demo

* simplify call a bit more

* remomve some comments

* use common.proto

* remove redundant browser-service layer, clean up naming

* delete mcp proto for now

* better client layout & easier service imports

* a reflection-based way to create grpc services automatically

* better code layout for grpc implementations

* switch to auto-generating the method registration via bash

* hook protobufs into package.json scripts

* make service implementations more generic

* warn user that they must install protoc deps

* delete old message passing for getBrowserConnectionInfo

* format fix

* format fix

* rewrite build-protos in node & update package.json

* don't protoc during package

* change how imports work based on feedback

* package lock seems necessary now

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-15 17:37:02 -07:00
Yusuke Mizushima dd84bdaa9e Fix/vertex token count (#2893)
* feat: add token usage metadata handling in VertexHandler

* feat: add cost calculation for API usage in VertexHandler

* fix: correct vertexai token count calculation
2025-04-15 17:22:46 -07:00
github-actions[bot] 4d8bdf2945 v3.12.3 Release Notes
v3.12.3 Release Notes

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-14 19:45:33 -07:00
pashpashpash ff3c840021 CLI for orchastrating automated evals (#2874)
* cli for evals

* preloading with multiple language extensions

* preloading with multiple language extensions

* moving to my repo

* test server
2025-04-14 19:29:49 -07:00
monotykamary 3cd2b18800 fix(api): update cacheReadsPrice for OpenAI GPT-4.1 models (#2887)
Set correct cacheReadsPrice (cached input price) for gpt-4.1, gpt-4.1 mini, and gpt-4.1 nano based on official OpenAI pricing. No changes to cacheWritesPrice as per current OpenAI documentation. This ensures prompt caching costs are accurately reflected for these models in cost calculations.
2025-04-14 18:34:39 -07:00
Toshii 0b19ba6023 NEW model update (#2892)
* new

* changeset
2025-04-14 17:57:03 -07:00
Evan 75143a718a add fetching global cline rules files (#2864)
* add fetching global cline rules files

* remove bad import from main merge

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-14 16:20:19 -07:00
Saoud Rizwan c69adcaf57 Remove mcpToolResult 2025-04-10 18:24:53 -07:00
rikaaa0928 697887b7af Merge branch 'remote' into feat-mcp-image 2025-04-05 11:22:57 +08:00
rikaaa0928 138206eae9 Merge branch 'remote' into feat-mcp-image 2025-04-04 13:46:01 +08:00
rikaaa0928 c5b714b59c Merge branch 'remote' into feat-mcp-image 2025-03-31 14:02:53 +08:00
rikaaa0928 7f60c08756 Merge branch 'main' into feat-mcp-image 2025-03-23 11:07:06 +08:00
rikaaa0928 ccaa999463 Merge branch 'remote' into feat-mcp-image 2025-03-22 16:48:32 +08:00
rikaaa0928 380b8c3aae Merge branch 'remote' into feat-mcp-image 2025-03-21 10:46:40 +08:00
rikaaa0928 9e94fd8e97 feat: MCP ImageContent support run format:fix 2025-03-08 15:28:51 +08:00
rikaaa0928 ff31ac1e1a Update src/core/Cline.ts
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-08 15:25:25 +08:00
rikaaa0928 59210b8d5d Update src/core/prompts/responses.ts
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-08 15:24:47 +08:00
rikaaa0928 30510dc73f feat: MCP ImageContent support changeset 2025-03-08 09:53:10 +08:00
rikaaa0928 eee5b9b77a feat: MCP ImageContent support 2025-03-08 09:39:59 +08:00
rikaaa0928 fd46b9c54e Merge branch 'remote' into feat-mcp-image 2025-03-08 09:05:30 +08:00
rikaaa0928 ee12bf5e30 feat: MCP ImageContent support 2025-03-08 09:02:13 +08:00
106 changed files with 8537 additions and 367 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added chunking to terminal outputs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Added copy button to MermaidBlock component
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
gRPC over vscode message bus to make messaging better
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow user to modify a previous message to continue chat
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add command to focus chat input
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix vertexai token count
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow user to send context with an option selection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
convert inline style to tailwind css of file `SettingsView.tsx`
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Safer check for xAI reasoning content
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add stream options back to xai provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add an edit button to the Cline Rules toggle modal to open rule files
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
MCP ImageContent as tool result
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Icons to indicate an action is occuring outside of the users workspace
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Readme update
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add OpenAI o3 & 4o-mini
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add modal UI for toggling Cline Rules
+6
View File
@@ -0,0 +1,6 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.svg,package-lock.json,*.css,.codespellrc,locales
check-hidden = true
ignore-regex = (\b(optIn|isTaller)\b|https://\S+)
# ignore-words-list =
+25
View File
@@ -0,0 +1,25 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
+8 -2
View File
@@ -1,5 +1,11 @@
# Changelog
## [3.12.3]
- Add copy button to MermaidBlock component (Thanks @cacosub7!)
- Add the ability to fetch from global cline rules files
- Add icon to indicate when a file outside of the users workspace is edited
## [3.12.2]
- Add gpt-4.1
@@ -52,7 +58,7 @@
- Add recommended models for Cline provider
- Add ability to detect when user edits files manually so Cline knows to re-read, leading to reduced diff edit errors
- Add improvements to file mention searching for faster searching
- Add scoring logic to file mentions to sort and exlcude results based on relevance
- Add scoring logic to file mentions to sort and exclude results based on relevance
- Add Support for Bytedance Doubao (Thanks Tunixer!)
- Fix to prevent duplicate BOM (Thanks bamps53!)
@@ -530,7 +536,7 @@
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
- Implement sliding window context management to keep tasks going past 200k tokens
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
- Improves system prompt to guard against lazy edits (less "//rest of code here")
## [1.3.0]
+1 -1
View File
@@ -2,7 +2,7 @@
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline
# Cline \#1 on OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+3
View File
@@ -0,0 +1,3 @@
repositories
results/evals.db
+162
View File
@@ -0,0 +1,162 @@
# Cline Evaluation System
This directory contains the evaluation system for benchmarking Cline against various coding evaluation frameworks.
## Overview
The Cline Evaluation System allows you to:
1. Run Cline against standardized coding benchmarks
2. Collect comprehensive metrics on performance
3. Generate detailed reports on evaluation results
4. Compare performance across different models and benchmarks
## Architecture
The evaluation system consists of two main components:
1. **Test Server**: Enhanced HTTP server in `src/services/test/TestServer.ts` that provides detailed task results
2. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
## Directory Structure
```
cline-repo/
├── src/
│ ├── services/
│ │ ├── test/
│ │ │ ├── TestServer.ts # Enhanced HTTP server for task execution
│ │ │ ├── GitHelper.ts # Git utilities for file tracking
│ │ │ └── ...
│ │ └── ...
│ └── ...
├── evals/ # Main directory for evaluation system
│ ├── cli/ # CLI tool for orchestrating evaluations
│ │ ├── src/
│ │ │ ├── index.ts # CLI entry point
│ │ │ ├── commands/ # CLI commands (setup, run, report)
│ │ │ ├── adapters/ # Benchmark adapters
│ │ │ ├── db/ # Database management
│ │ │ └── utils/ # Utility functions
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── repositories/ # Cloned benchmark repositories
│ │ ├── exercism/ # Modified Exercism (from pashpashpash/evals)
│ │ ├── swe-bench/ # SWE-Bench repository
│ │ ├── swelancer/ # SWELancer repository
│ │ └── multi-swe/ # Multi-SWE-Bench repository
│ ├── results/ # Evaluation results storage
│ │ ├── runs/ # Individual run results
│ │ └── reports/ # Generated reports
│ └── README.md # This file
└── ...
```
## Getting Started
### Prerequisites
- Node.js 16+
- VSCode with Cline extension installed
- Git
### Installation
1. Build the CLI tool:
```bash
cd evals/cli
npm install
npm run build
```
### Usage
#### Setting Up Benchmarks
```bash
cd evals/cli
node dist/index.js setup
```
This will clone and set up all benchmark repositories. You can specify specific benchmarks:
```bash
node dist/index.js setup --benchmarks exercism
```
#### Running Evaluations
```bash
node dist/index.js run --model claude-3-opus-20240229 --benchmark exercism
```
Options:
- `--model`: The model to evaluate (default: claude-3-opus-20240229)
- `--benchmark`: Specific benchmark to run (default: all)
- `--count`: Number of tasks to run (default: all)
#### Generating Reports
```bash
node dist/index.js report
```
Options:
- `--format`: Report format (json, markdown) (default: markdown)
- `--output`: Output path for the report
## Benchmarks
### Exercism
Modified Exercism exercises from the [pashpashpash/evals](https://github.com/pashpashpash/evals) repository. These are small, focused programming exercises in various languages.
### SWE-Bench (Coming Soon)
Real-world software engineering tasks from the [SWE-bench](https://github.com/SWE-bench/SWE-bench) repository.
### SWELancer (Coming Soon)
Freelance-style programming tasks from the SWELancer benchmark.
### Multi-SWE-Bench (Coming Soon)
Multi-file software engineering tasks from the Multi-SWE-Bench repository.
## Metrics
The evaluation system collects the following metrics:
- **Token Usage**: Input and output tokens
- **Cost**: Estimated cost of API calls
- **Duration**: Time taken to complete tasks
- **Tool Usage**: Number of tool calls and failures
- **Success Rate**: Percentage of tasks completed successfully
- **Functional Correctness**: Percentage of tests passed
## Reports
Reports are generated in Markdown or JSON format and include:
- Overall summary
- Benchmark-specific results
- Model-specific results
- Tool usage statistics
- Charts and visualizations
## Development
### Adding a New Benchmark
1. Create a new adapter in `evals/cli/src/adapters/`
2. Implement the `BenchmarkAdapter` interface
3. Register the adapter in `evals/cli/src/adapters/index.ts`
### Extending Metrics
To add new metrics:
1. Update the database schema in `evals/cli/src/db/schema.ts`
2. Add collection logic in `evals/cli/src/utils/results.ts`
3. Update report generation in `evals/cli/src/commands/report.ts`
+2455
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
{
"name": "cline-evaluation-cli",
"version": "0.1.0",
"description": "CLI tool for orchestrating Cline evaluations across multiple benchmarks",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cline",
"evaluation",
"benchmark"
],
"author": "",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.0.0",
"chalk": "^4.1.2",
"commander": "^9.4.1",
"execa": "^5.1.1",
"node-fetch": "^2.7.0",
"ora": "^5.4.1",
"sqlite": "^4.1.2",
"uuid": "^9.0.0",
"yargs": "^17.6.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.3",
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.12",
"@types/uuid": "^9.0.0",
"@types/yargs": "^17.0.19",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}
+190
View File
@@ -0,0 +1,190 @@
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
const EVALS_DIR = path.resolve(__dirname, "../../../")
/**
* Adapter for the modified Exercism benchmark
*/
export class ExercismAdapter implements BenchmarkAdapter {
name = "exercism"
/**
* Set up the Exercism benchmark repository
*/
async setup(): Promise<void> {
// Clone repository if needed
const exercismDir = path.join(EVALS_DIR, "repositories", "exercism")
if (!fs.existsSync(exercismDir)) {
console.log(`Cloning Exercism repository to ${exercismDir}...`)
await execa("git", ["clone", "https://github.com/pashpashpash/evals.git", exercismDir])
console.log("Exercism repository cloned successfully")
} else {
console.log(`Exercism repository already exists at ${exercismDir}`)
// Pull latest changes
console.log("Pulling latest changes...")
await execa("git", ["pull"], { cwd: exercismDir })
console.log("Repository updated successfully")
}
}
/**
* List all available tasks in the Exercism benchmark
*/
async listTasks(): Promise<Task[]> {
const tasks: Task[] = []
const exercisesDir = path.join(EVALS_DIR, "repositories", "exercism")
// Ensure the repository exists
if (!fs.existsSync(exercisesDir)) {
throw new Error(`Exercism repository not found at ${exercisesDir}. Run setup first.`)
}
// Read language directories
const languages = fs
.readdirSync(exercisesDir)
.filter((dir) => fs.statSync(path.join(exercisesDir, dir)).isDirectory())
.filter((dir) => !dir.startsWith(".") && !["node_modules", ".git"].includes(dir))
for (const language of languages) {
const languageDir = path.join(exercisesDir, language)
// Read exercise directories
const exercises = fs.readdirSync(languageDir).filter((dir) => fs.statSync(path.join(languageDir, dir)).isDirectory())
for (const exercise of exercises) {
const exerciseDir = path.join(languageDir, exercise)
// Read instructions
let description = ""
const instructionsPath = path.join(exerciseDir, "docs", "instructions.md")
if (fs.existsSync(instructionsPath)) {
description = fs.readFileSync(instructionsPath, "utf-8")
}
// Determine test commands based on language
let testCommands: string[] = []
switch (language) {
case "javascript":
testCommands = ["npm install", "npm test"]
break
case "python":
testCommands = ["python -m pytest -o markers=task *_test.py"]
break
case "go":
testCommands = ["go test"]
break
case "java":
testCommands = ["./gradlew test"]
break
case "rust":
testCommands = ["cargo test"]
break
default:
testCommands = []
}
tasks.push({
id: `exercism-${language}-${exercise}`,
name: exercise,
description,
workspacePath: exerciseDir,
setupCommands: [],
verificationCommands: testCommands,
metadata: {
language,
type: "exercism",
},
})
}
}
return tasks
}
/**
* Prepare a specific task for execution
* @param taskId The ID of the task to prepare
*/
async prepareTask(taskId: string): Promise<Task> {
const tasks = await this.listTasks()
const task = tasks.find((t) => t.id === taskId)
if (!task) {
throw new Error(`Task ${taskId} not found`)
}
// Check if Git repository is already initialized
const gitDirExists = fs.existsSync(path.join(task.workspacePath, ".git"))
try {
// Initialize Git repository if needed
if (!gitDirExists) {
await execa("git", ["init"], { cwd: task.workspacePath })
}
// Create a dummy file to ensure there's something to commit
const dummyFilePath = path.join(task.workspacePath, ".eval-timestamp")
fs.writeFileSync(dummyFilePath, new Date().toISOString())
// Add all files and commit
await execa("git", ["add", "."], { cwd: task.workspacePath })
try {
await execa("git", ["commit", "-m", "Initial commit"], { cwd: task.workspacePath })
} catch (error: any) {
// If commit fails because there are no changes, that's okay
if (!error.stderr?.includes("nothing to commit")) {
throw error
}
}
} catch (error: any) {
console.warn(`Warning: Git operations failed: ${error.message}`)
console.warn("Continuing without Git initialization")
}
return task
}
/**
* Verify the result of a task execution
* @param task The task that was executed
* @param result The result of the task execution
*/
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
// Run verification commands
let success = true
let output = ""
for (const command of task.verificationCommands) {
try {
const [cmd, ...args] = command.split(" ")
const { stdout } = await execa(cmd, args, { cwd: task.workspacePath })
output += stdout + "\n"
} catch (error: any) {
success = false
if (error.stdout) output += error.stdout + "\n"
if (error.stderr) output += error.stderr + "\n"
}
}
// Parse test results
const testsPassed = (output.match(/PASS/g) || []).length
const testsFailed = (output.match(/FAIL/g) || []).length
const testsTotal = testsPassed + testsFailed
return {
success,
metrics: {
testsPassed,
testsFailed,
testsTotal,
functionalCorrectness: testsTotal > 0 ? testsPassed / testsTotal : 0,
},
}
}
}
+47
View File
@@ -0,0 +1,47 @@
import { BenchmarkAdapter } from "./types"
import { ExercismAdapter } from "./exercism"
import { SWEBenchAdapter } from "./swe-bench"
import { SWELancerAdapter } from "./swelancer"
import { MultiSWEAdapter } from "./multi-swe"
// Registry of all available adapters
const adapters: Record<string, BenchmarkAdapter> = {
// Exercism is the primary adapter with real implementation
exercism: new ExercismAdapter(),
// Dummy adapters for testing
"swe-bench": new SWEBenchAdapter(),
swelancer: new SWELancerAdapter(),
"multi-swe": new MultiSWEAdapter(),
}
/**
* Get a specific adapter by name
* @param name The name of the adapter to get
* @returns The requested adapter
* @throws Error if the adapter is not found
*/
export function getAdapter(name: string): BenchmarkAdapter {
const adapter = adapters[name]
if (!adapter) {
throw new Error(`Adapter for benchmark '${name}' not found`)
}
return adapter
}
/**
* Get all available adapters
* @returns Array of all registered adapters
*/
export function getAllAdapters(): BenchmarkAdapter[] {
return Object.values(adapters)
}
/**
* Register a new adapter
* @param name The name to register the adapter under
* @param adapter The adapter to register
*/
export function registerAdapter(name: string, adapter: BenchmarkAdapter): void {
adapters[name] = adapter
}
+192
View File
@@ -0,0 +1,192 @@
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
const EVALS_DIR = path.resolve(__dirname, "../../../")
/**
* Dummy adapter for the Multi-SWE-Bench benchmark
*/
export class MultiSWEAdapter implements BenchmarkAdapter {
name = "multi-swe"
/**
* Set up the Multi-SWE-Bench benchmark repository (dummy implementation)
*/
async setup(): Promise<void> {
console.log("Multi-SWE-Bench dummy setup completed")
// Create repositories directory if it doesn't exist
const repoDir = path.join(EVALS_DIR, "repositories", "multi-swe")
if (!fs.existsSync(repoDir)) {
fs.mkdirSync(repoDir, { recursive: true })
console.log(`Created dummy Multi-SWE-Bench directory at ${repoDir}`)
}
}
/**
* List all available tasks in the Multi-SWE-Bench benchmark (dummy implementation)
*/
async listTasks(): Promise<Task[]> {
return [
{
id: "multi-swe-task-1",
name: "Multi-Language API Integration",
description:
"Implement a system that integrates a Python backend with a TypeScript frontend and a Rust processing service.",
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
setupCommands: [],
verificationCommands: [],
metadata: {
languages: ["python", "typescript", "rust"],
complexity: "high",
type: "multi-swe",
},
},
{
id: "multi-swe-task-2",
name: "Cross-Platform Mobile App",
description: "Create a cross-platform mobile app using React Native with native modules in Swift and Kotlin.",
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
setupCommands: [],
verificationCommands: [],
metadata: {
languages: ["javascript", "swift", "kotlin"],
complexity: "medium",
type: "multi-swe",
},
},
{
id: "multi-swe-task-3",
name: "Microservice Architecture",
description: "Design and implement a microservice architecture with services written in Go, Node.js, and Java.",
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
setupCommands: [],
verificationCommands: [],
metadata: {
languages: ["go", "javascript", "java"],
complexity: "high",
type: "multi-swe",
},
},
]
}
/**
* Prepare a specific task for execution (dummy implementation)
* @param taskId The ID of the task to prepare
*/
async prepareTask(taskId: string): Promise<Task> {
const tasks = await this.listTasks()
const task = tasks.find((t) => t.id === taskId)
if (!task) {
throw new Error(`Task ${taskId} not found`)
}
// Create a dummy workspace for the task
const taskDir = path.join(task.workspacePath, taskId)
if (!fs.existsSync(taskDir)) {
fs.mkdirSync(taskDir, { recursive: true })
// Create a dummy file for the task
fs.writeFileSync(
path.join(taskDir, "README.md"),
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
)
// Create additional dummy files based on task type
if (task.id === "multi-swe-task-1") {
// Python backend
fs.mkdirSync(path.join(taskDir, "backend"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "backend", "app.py"),
`# TODO: Implement Python backend\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return "Hello, World!"\n`,
)
// TypeScript frontend
fs.mkdirSync(path.join(taskDir, "frontend"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "frontend", "app.ts"),
`// TODO: Implement TypeScript frontend\nconsole.log('Frontend starting...');\n`,
)
// Rust processing service
fs.mkdirSync(path.join(taskDir, "processor"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "processor", "main.rs"),
`// TODO: Implement Rust processing service\nfn main() {\n println!("Processor starting...");\n}\n`,
)
} else if (task.id === "multi-swe-task-2") {
// React Native app
fs.mkdirSync(path.join(taskDir, "app"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "app", "App.js"),
`// TODO: Implement React Native app\nimport React from 'react';\nimport { View, Text } from 'react-native';\n\nexport default function App() {\n return (\n <View>\n <Text>Hello, World!</Text>\n </View>\n );\n}\n`,
)
// Swift native module
fs.mkdirSync(path.join(taskDir, "ios"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "ios", "NativeModule.swift"),
`// TODO: Implement Swift native module\nimport Foundation\n\n@objc(NativeModule)\nclass NativeModule: NSObject {\n @objc\n func hello() -> String {\n return "Hello from Swift"\n }\n}\n`,
)
// Kotlin native module
fs.mkdirSync(path.join(taskDir, "android"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "android", "NativeModule.kt"),
`// TODO: Implement Kotlin native module\npackage com.example.app\n\nclass NativeModule {\n fun hello(): String {\n return "Hello from Kotlin"\n }\n}\n`,
)
} else if (task.id === "multi-swe-task-3") {
// Go service
fs.mkdirSync(path.join(taskDir, "service-go"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "service-go", "main.go"),
`// TODO: Implement Go service\npackage main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Go service starting...")\n}\n`,
)
// Node.js service
fs.mkdirSync(path.join(taskDir, "service-node"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "service-node", "server.js"),
`// TODO: Implement Node.js service\nconsole.log('Node.js service starting...');\n`,
)
// Java service
fs.mkdirSync(path.join(taskDir, "service-java"), { recursive: true })
fs.writeFileSync(
path.join(taskDir, "service-java", "Main.java"),
`// TODO: Implement Java service\npublic class Main {\n public static void main(String[] args) {\n System.out.println("Java service starting...");\n }\n}\n`,
)
}
}
// Update the task's workspace path to the task-specific directory
return {
...task,
workspacePath: taskDir,
}
}
/**
* Verify the result of a task execution (dummy implementation)
* @param task The task that was executed
* @param result The result of the task execution
*/
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
// Always return success for dummy implementation
return {
success: true,
metrics: {
testsPassed: 1,
testsFailed: 0,
testsTotal: 1,
functionalCorrectness: 1.0,
crossLanguageIntegration: 0.9, // Dummy metric specific to Multi-SWE
architectureQuality: 0.85, // Dummy metric specific to Multi-SWE
},
}
}
}
+125
View File
@@ -0,0 +1,125 @@
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
const EVALS_DIR = path.resolve(__dirname, "../../../")
/**
* Dummy adapter for the SWE-Bench benchmark
*/
export class SWEBenchAdapter implements BenchmarkAdapter {
name = "swe-bench"
/**
* Set up the SWE-Bench benchmark repository (dummy implementation)
*/
async setup(): Promise<void> {
console.log("SWE-Bench dummy setup completed")
// Create repositories directory if it doesn't exist
const repoDir = path.join(EVALS_DIR, "repositories", "swe-bench")
if (!fs.existsSync(repoDir)) {
fs.mkdirSync(repoDir, { recursive: true })
console.log(`Created dummy SWE-Bench directory at ${repoDir}`)
}
}
/**
* List all available tasks in the SWE-Bench benchmark (dummy implementation)
*/
async listTasks(): Promise<Task[]> {
return [
{
id: "swe-bench-task-1",
name: "Fix React Component Bug",
description: "Fix a bug in a React component where the state is not properly updated.",
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
setupCommands: [],
verificationCommands: [],
metadata: {
repository: "facebook/react",
issue: "#12345",
type: "swe-bench",
},
},
{
id: "swe-bench-task-2",
name: "Optimize Database Query",
description: "Optimize a slow database query in a Django application.",
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
setupCommands: [],
verificationCommands: [],
metadata: {
repository: "django/django",
issue: "#6789",
type: "swe-bench",
},
},
{
id: "swe-bench-task-3",
name: "Fix Memory Leak",
description: "Fix a memory leak in a Node.js application.",
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
setupCommands: [],
verificationCommands: [],
metadata: {
repository: "nodejs/node",
issue: "#9876",
type: "swe-bench",
},
},
]
}
/**
* Prepare a specific task for execution (dummy implementation)
* @param taskId The ID of the task to prepare
*/
async prepareTask(taskId: string): Promise<Task> {
const tasks = await this.listTasks()
const task = tasks.find((t) => t.id === taskId)
if (!task) {
throw new Error(`Task ${taskId} not found`)
}
// Create a dummy workspace for the task
const taskDir = path.join(task.workspacePath, taskId)
if (!fs.existsSync(taskDir)) {
fs.mkdirSync(taskDir, { recursive: true })
// Create a dummy file for the task
fs.writeFileSync(
path.join(taskDir, "README.md"),
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
)
}
// Update the task's workspace path to the task-specific directory
return {
...task,
workspacePath: taskDir,
}
}
/**
* Verify the result of a task execution (dummy implementation)
* @param task The task that was executed
* @param result The result of the task execution
*/
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
// Always return success for dummy implementation
return {
success: true,
metrics: {
testsPassed: 1,
testsFailed: 0,
testsTotal: 1,
functionalCorrectness: 1.0,
performanceImprovement: 0.25, // Dummy metric specific to SWE-Bench
codeQuality: 0.9, // Dummy metric specific to SWE-Bench
},
}
}
}
+143
View File
@@ -0,0 +1,143 @@
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
const EVALS_DIR = path.resolve(__dirname, "../../../")
/**
* Dummy adapter for the SWELancer benchmark
*/
export class SWELancerAdapter implements BenchmarkAdapter {
name = "swelancer"
/**
* Set up the SWELancer benchmark repository (dummy implementation)
*/
async setup(): Promise<void> {
console.log("SWELancer dummy setup completed")
// Create repositories directory if it doesn't exist
const repoDir = path.join(EVALS_DIR, "repositories", "swelancer")
if (!fs.existsSync(repoDir)) {
fs.mkdirSync(repoDir, { recursive: true })
console.log(`Created dummy SWELancer directory at ${repoDir}`)
}
}
/**
* List all available tasks in the SWELancer benchmark (dummy implementation)
*/
async listTasks(): Promise<Task[]> {
return [
{
id: "swelancer-task-1",
name: "Create Landing Page",
description: "Create a responsive landing page for a new product using HTML, CSS, and JavaScript.",
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
setupCommands: [],
verificationCommands: [],
metadata: {
client: "TechStartup Inc.",
difficulty: "medium",
type: "swelancer",
},
},
{
id: "swelancer-task-2",
name: "Build REST API",
description: "Create a RESTful API for a blog application using Node.js and Express.",
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
setupCommands: [],
verificationCommands: [],
metadata: {
client: "BlogCo",
difficulty: "hard",
type: "swelancer",
},
},
{
id: "swelancer-task-3",
name: "Fix CSS Layout Issues",
description: "Fix layout issues in a responsive website across different screen sizes.",
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
setupCommands: [],
verificationCommands: [],
metadata: {
client: "DesignAgency",
difficulty: "easy",
type: "swelancer",
},
},
]
}
/**
* Prepare a specific task for execution (dummy implementation)
* @param taskId The ID of the task to prepare
*/
async prepareTask(taskId: string): Promise<Task> {
const tasks = await this.listTasks()
const task = tasks.find((t) => t.id === taskId)
if (!task) {
throw new Error(`Task ${taskId} not found`)
}
// Create a dummy workspace for the task
const taskDir = path.join(task.workspacePath, taskId)
if (!fs.existsSync(taskDir)) {
fs.mkdirSync(taskDir, { recursive: true })
// Create a dummy file for the task
fs.writeFileSync(
path.join(taskDir, "README.md"),
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
)
// Create additional dummy files based on task type
if (task.id === "swelancer-task-1") {
fs.writeFileSync(
path.join(taskDir, "index.html"),
`<!DOCTYPE html>\n<html>\n<head>\n <title>Landing Page</title>\n</head>\n<body>\n <!-- TODO: Implement landing page -->\n</body>\n</html>`,
)
} else if (task.id === "swelancer-task-2") {
fs.writeFileSync(
path.join(taskDir, "server.js"),
`// TODO: Implement REST API\nconsole.log('Server starting...');`,
)
} else if (task.id === "swelancer-task-3") {
fs.writeFileSync(
path.join(taskDir, "styles.css"),
`/* TODO: Fix layout issues */\nbody {\n margin: 0;\n padding: 0;\n}`,
)
}
}
// Update the task's workspace path to the task-specific directory
return {
...task,
workspacePath: taskDir,
}
}
/**
* Verify the result of a task execution (dummy implementation)
* @param task The task that was executed
* @param result The result of the task execution
*/
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
// Always return success for dummy implementation
return {
success: true,
metrics: {
testsPassed: 1,
testsFailed: 0,
testsTotal: 1,
functionalCorrectness: 1.0,
clientSatisfaction: 0.95, // Dummy metric specific to SWELancer
timeEfficiency: 0.85, // Dummy metric specific to SWELancer
},
}
}
}
+31
View File
@@ -0,0 +1,31 @@
/**
* Represents a task to be executed
*/
export interface Task {
id: string
name: string
description: string
workspacePath: string
setupCommands: string[]
verificationCommands: string[]
metadata: Record<string, any>
}
/**
* Result of verifying a task execution
*/
export interface VerificationResult {
success: boolean
metrics: Record<string, any>
}
/**
* Interface for benchmark adapters
*/
export interface BenchmarkAdapter {
name: string
setup(): Promise<void>
listTasks(): Promise<Task[]>
prepareTask(taskId: string): Promise<Task>
verifyResult(task: Task, result: any): Promise<VerificationResult>
}
+237
View File
@@ -0,0 +1,237 @@
import * as fs from "fs"
import * as path from "path"
import chalk from "chalk"
import ora from "ora"
import { ResultsDatabase } from "../db"
import { generateMarkdownReport } from "../utils/markdown"
interface ReportOptions {
format?: "json" | "markdown"
output?: string
}
/**
* Handler for the report command
* @param options Command options
*/
export async function reportHandler(options: ReportOptions): Promise<void> {
const format = options.format || "markdown"
const db = new ResultsDatabase()
try {
const spinner = ora("Generating report...").start()
// Get all runs
const runs = db.getRuns()
console.log(chalk.blue(`Found ${runs.length} evaluation runs`))
if (runs.length === 0) {
spinner.fail("No evaluation runs found")
return
}
// Generate summary report
const summary = {
runs: runs.length,
models: [...new Set(runs.map((run) => run.model))],
benchmarks: [...new Set(runs.map((run) => run.benchmark))],
tasks: 0,
successRate: 0,
averageTokens: 0,
averageCost: 0,
averageDuration: 0,
totalToolCalls: 0,
totalToolFailures: 0,
toolSuccessRate: 0,
toolUsage: {} as Record<string, { calls: number; failures: number }>,
}
let totalTasks = 0
let successfulTasks = 0
let totalTokens = 0
let totalCost = 0
let totalDuration = 0
let totalToolCalls = 0
let totalToolFailures = 0
for (const run of runs) {
const tasks = db.getRunTasks(run.id)
totalTasks += tasks.length
for (const task of tasks) {
if (task.success) {
successfulTasks++
}
const metrics = db.getTaskMetrics(task.id)
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
totalTokens += tokensIn + tokensOut
totalCost += metrics.find((m) => m.name === "cost")?.value || 0
totalDuration += metrics.find((m) => m.name === "duration")?.value || 0
// Collect tool call metrics
totalToolCalls += task.total_tool_calls || 0
totalToolFailures += task.total_tool_failures || 0
// Get detailed tool usage
const toolCalls = db.getTaskToolCalls(task.id)
for (const toolCall of toolCalls) {
if (!summary.toolUsage[toolCall.tool_name]) {
summary.toolUsage[toolCall.tool_name] = {
calls: 0,
failures: 0,
}
}
summary.toolUsage[toolCall.tool_name].calls += toolCall.call_count
summary.toolUsage[toolCall.tool_name].failures += toolCall.failure_count
}
}
}
// Calculate tool success rate
summary.totalToolCalls = totalToolCalls
summary.totalToolFailures = totalToolFailures
summary.toolSuccessRate = totalToolCalls > 0 ? 1 - totalToolFailures / totalToolCalls : 1.0
summary.tasks = totalTasks
summary.successRate = totalTasks > 0 ? successfulTasks / totalTasks : 0
summary.averageTokens = totalTasks > 0 ? totalTokens / totalTasks : 0
summary.averageCost = totalTasks > 0 ? totalCost / totalTasks : 0
summary.averageDuration = totalTasks > 0 ? totalDuration / totalTasks : 0
// Generate benchmark-specific reports
const benchmarkReports: Record<string, any> = {}
for (const benchmark of summary.benchmarks) {
const benchmarkRuns = runs.filter((run) => run.benchmark === benchmark)
const benchmarkSummary = {
runs: benchmarkRuns.length,
models: [...new Set(benchmarkRuns.map((run) => run.model))],
tasks: 0,
successRate: 0,
averageTokens: 0,
averageCost: 0,
averageDuration: 0,
}
let benchmarkTasks = 0
let benchmarkSuccessfulTasks = 0
let benchmarkTotalTokens = 0
let benchmarkTotalCost = 0
let benchmarkTotalDuration = 0
for (const run of benchmarkRuns) {
const tasks = db.getRunTasks(run.id)
benchmarkTasks += tasks.length
for (const task of tasks) {
if (task.success) {
benchmarkSuccessfulTasks++
}
const metrics = db.getTaskMetrics(task.id)
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
benchmarkTotalTokens += tokensIn + tokensOut
benchmarkTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
benchmarkTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
}
}
benchmarkSummary.tasks = benchmarkTasks
benchmarkSummary.successRate = benchmarkTasks > 0 ? benchmarkSuccessfulTasks / benchmarkTasks : 0
benchmarkSummary.averageTokens = benchmarkTasks > 0 ? benchmarkTotalTokens / benchmarkTasks : 0
benchmarkSummary.averageCost = benchmarkTasks > 0 ? benchmarkTotalCost / benchmarkTasks : 0
benchmarkSummary.averageDuration = benchmarkTasks > 0 ? benchmarkTotalDuration / benchmarkTasks : 0
benchmarkReports[benchmark] = benchmarkSummary
}
// Generate model-specific reports
const modelReports: Record<string, any> = {}
for (const model of summary.models) {
const modelRuns = runs.filter((run) => run.model === model)
const modelSummary = {
runs: modelRuns.length,
benchmarks: [...new Set(modelRuns.map((run) => run.benchmark))],
tasks: 0,
successRate: 0,
averageTokens: 0,
averageCost: 0,
averageDuration: 0,
}
let modelTasks = 0
let modelSuccessfulTasks = 0
let modelTotalTokens = 0
let modelTotalCost = 0
let modelTotalDuration = 0
for (const run of modelRuns) {
const tasks = db.getRunTasks(run.id)
modelTasks += tasks.length
for (const task of tasks) {
if (task.success) {
modelSuccessfulTasks++
}
const metrics = db.getTaskMetrics(task.id)
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
modelTotalTokens += tokensIn + tokensOut
modelTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
modelTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
}
}
modelSummary.tasks = modelTasks
modelSummary.successRate = modelTasks > 0 ? modelSuccessfulTasks / modelTasks : 0
modelSummary.averageTokens = modelTasks > 0 ? modelTotalTokens / modelTasks : 0
modelSummary.averageCost = modelTasks > 0 ? modelTotalCost / modelTasks : 0
modelSummary.averageDuration = modelTasks > 0 ? modelTotalDuration / modelTasks : 0
modelReports[model] = modelSummary
}
// Save reports
const reportDir = path.join(path.resolve(__dirname, "../../../"), "results", "reports")
fs.mkdirSync(reportDir, { recursive: true })
const timestamp = new Date().toISOString().replace(/:/g, "-")
if (format === "json") {
// Save JSON reports
fs.writeFileSync(path.join(reportDir, `summary-${timestamp}.json`), JSON.stringify(summary, null, 2))
fs.writeFileSync(path.join(reportDir, `benchmarks-${timestamp}.json`), JSON.stringify(benchmarkReports, null, 2))
fs.writeFileSync(path.join(reportDir, `models-${timestamp}.json`), JSON.stringify(modelReports, null, 2))
spinner.succeed(`JSON reports generated in ${reportDir}`)
} else {
// Generate markdown report
const outputPath = options.output || path.join(reportDir, `report-${timestamp}.md`)
generateMarkdownReport(summary, benchmarkReports, modelReports, outputPath)
spinner.succeed(`Markdown report generated at ${outputPath}`)
}
} catch (error: any) {
console.error(chalk.red(`Error generating report: ${error.message}`))
console.error(error.stack)
} finally {
db.close()
}
}
+133
View File
@@ -0,0 +1,133 @@
import * as path from "path"
import { v4 as uuidv4 } from "uuid"
import chalk from "chalk"
import ora from "ora"
import { getAdapter } from "../adapters"
import { ResultsDatabase } from "../db"
import { spawnVSCode, cleanupVSCode } from "../utils/vscode"
import { sendTaskToServer } from "../utils/task"
import { storeTaskResult } from "../utils/results"
interface RunOptions {
benchmark?: string
model: string
count?: number
apiKey?: string
}
/**
* Handler for the run command
* @param options Command options
*/
export async function runHandler(options: RunOptions): Promise<void> {
// Determine which benchmarks to run
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism for now
const model = options.model
const count = options.count || Infinity
console.log(chalk.blue(`Running evaluations for model: ${model}`))
console.log(chalk.blue(`Benchmarks: ${benchmarks.join(", ")}`))
// Create a run for each benchmark
for (const benchmark of benchmarks) {
const runId = uuidv4()
const db = new ResultsDatabase()
console.log(chalk.green(`\nStarting run for benchmark: ${benchmark}`))
// Create run in database
db.createRun(runId, model, benchmark)
// Get adapter for this benchmark
try {
const adapter = getAdapter(benchmark)
// List tasks
const spinner = ora("Listing tasks...").start()
const tasks = await adapter.listTasks()
spinner.succeed(`Found ${tasks.length} tasks for ${benchmark}`)
// Limit number of tasks if specified
const tasksToRun = tasks.slice(0, count)
console.log(chalk.blue(`Running ${tasksToRun.length} tasks...`))
// Run each task
for (let i = 0; i < tasksToRun.length; i++) {
const task = tasksToRun[i]
console.log(chalk.cyan(`\nTask ${i + 1}/${tasksToRun.length}: ${task.name}`))
// Prepare task
const prepareSpinner = ora("Preparing task...").start()
const preparedTask = await adapter.prepareTask(task.id)
prepareSpinner.succeed("Task prepared")
// Spawn VSCode
console.log("Spawning VSCode...")
await spawnVSCode(preparedTask.workspacePath)
// Send task to server
const sendSpinner = ora("Sending task to server...").start()
try {
const result = await sendTaskToServer(preparedTask.description, options.apiKey)
sendSpinner.succeed("Task completed")
// Verify result
const verifySpinner = ora("Verifying result...").start()
const verification = await adapter.verifyResult(preparedTask, result)
if (verification.success) {
verifySpinner.succeed(
`Verification successful: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
)
} else {
verifySpinner.fail(
`Verification failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
)
}
// Store result
const storeSpinner = ora("Storing result...").start()
await storeTaskResult(runId, preparedTask, result, verification)
storeSpinner.succeed("Result stored")
console.log(chalk.green(`Task completed. Success: ${verification.success}`))
// Clean up VS Code and temporary files
const cleanupSpinner = ora("Cleaning up...").start()
try {
await cleanupVSCode(preparedTask.workspacePath)
cleanupSpinner.succeed("Cleanup completed")
} catch (cleanupError: any) {
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
console.error(chalk.yellow(cleanupError.stack))
}
} catch (error: any) {
sendSpinner.fail(`Task failed: ${error.message}`)
console.error(chalk.red(error.stack))
// Clean up VS Code and temporary files even if the task failed
const cleanupSpinner = ora("Cleaning up...").start()
try {
await cleanupVSCode(preparedTask.workspacePath)
cleanupSpinner.succeed("Cleanup completed")
} catch (cleanupError: any) {
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
console.error(chalk.yellow(cleanupError.stack))
}
}
}
// Mark run as complete
db.completeRun(runId)
console.log(chalk.green(`\nRun complete for benchmark: ${benchmark}`))
} catch (error: any) {
console.error(chalk.red(`Error running benchmark ${benchmark}: ${error.message}`))
console.error(error.stack)
}
}
console.log(chalk.green("\nAll evaluations complete"))
}
+72
View File
@@ -0,0 +1,72 @@
import * as path from "path"
import * as fs from "fs"
import execa from "execa"
import chalk from "chalk"
import ora from "ora"
import { getAllAdapters } from "../adapters/index"
import { BenchmarkAdapter } from "../adapters/types"
interface SetupOptions {
benchmarks: string
}
/**
* Handler for the setup command
* @param options Command options
*/
export async function setupHandler(options: SetupOptions): Promise<void> {
const benchmarks = options.benchmarks.split(",")
console.log(chalk.blue(`Setting up benchmarks: ${benchmarks.join(", ")}`))
// Create directories
const evalsDir = path.resolve(__dirname, "../../../")
const reposDir = path.join(evalsDir, "repositories")
const resultsDir = path.join(evalsDir, "results")
const spinner = ora("Creating directory structure").start()
try {
fs.mkdirSync(reposDir, { recursive: true })
fs.mkdirSync(resultsDir, { recursive: true })
fs.mkdirSync(path.join(resultsDir, "runs"), { recursive: true })
fs.mkdirSync(path.join(resultsDir, "reports"), { recursive: true })
spinner.succeed("Directory structure created")
} catch (error) {
spinner.fail(`Failed to create directory structure: ${(error as Error).message}`)
throw error
}
// Set up each benchmark
try {
const adapters = getAllAdapters().filter((adapter: BenchmarkAdapter) => benchmarks.includes(adapter.name))
if (adapters.length === 0) {
console.warn(chalk.yellow("No valid benchmarks specified. Available benchmarks:"))
console.warn(
chalk.yellow(
getAllAdapters()
.map((a: BenchmarkAdapter) => a.name)
.join(", "),
),
)
return
}
for (const adapter of adapters) {
const setupSpinner = ora(`Setting up ${adapter.name}...`).start()
try {
await adapter.setup()
setupSpinner.succeed(`${adapter.name} setup complete`)
} catch (error) {
setupSpinner.fail(`Failed to set up ${adapter.name}: ${(error as Error).message}`)
throw error
}
}
console.log(chalk.green("Setup complete"))
} catch (error) {
console.error(chalk.red(`Setup failed: ${(error as Error).message}`))
throw error
}
}
+211
View File
@@ -0,0 +1,211 @@
import * as path from "path"
import * as fs from "fs"
import Database from "better-sqlite3"
import { SCHEMA } from "./schema"
const EVALS_DIR = path.resolve(__dirname, "../../../")
/**
* Database class for storing evaluation results
*/
export class ResultsDatabase {
db: Database.Database
constructor() {
// Ensure results directory exists
const resultsDir = path.join(EVALS_DIR, "results")
fs.mkdirSync(resultsDir, { recursive: true })
// Create database file
const dbPath = path.join(resultsDir, "evals.db")
this.db = new Database(dbPath)
// Initialize schema
this.initSchema()
}
/**
* Initialize the database schema
*/
private initSchema(): void {
this.db.exec(SCHEMA)
}
/**
* Create a new evaluation run
* @param id Run ID
* @param model Model name
* @param benchmark Benchmark name
*/
createRun(id: string, model: string, benchmark: string): void {
const stmt = this.db.prepare(`
INSERT INTO runs (id, timestamp, model, benchmark)
VALUES (?, ?, ?, ?)
`)
stmt.run(id, Date.now(), model, benchmark)
}
/**
* Mark a run as completed
* @param id Run ID
*/
completeRun(id: string): void {
const stmt = this.db.prepare(`
UPDATE runs SET completed = 1 WHERE id = ?
`)
stmt.run(id)
}
/**
* Create a new task
* @param id Task ID
* @param runId Run ID
* @param taskId Original task ID
*/
createTask(id: string, runId: string, taskId: string): void {
const stmt = this.db.prepare(`
INSERT INTO tasks (id, run_id, task_id, timestamp)
VALUES (?, ?, ?, ?)
`)
stmt.run(id, runId, taskId, Date.now())
}
/**
* Mark a task as completed
* @param id Task ID
* @param success Whether the task was successful
* @param toolCalls Total tool calls
* @param toolFailures Total tool failures
*/
completeTask(id: string, success: boolean, toolCalls: number = 0, toolFailures: number = 0): void {
const stmt = this.db.prepare(`
UPDATE tasks
SET success = ?, total_tool_calls = ?, total_tool_failures = ?
WHERE id = ?
`)
stmt.run(success ? 1 : 0, toolCalls, toolFailures, id)
}
/**
* Add a metric to a task
* @param taskId Task ID
* @param name Metric name
* @param value Metric value
*/
addMetric(taskId: string, name: string, value: number): void {
const stmt = this.db.prepare(`
INSERT INTO metrics (task_id, name, value)
VALUES (?, ?, ?)
`)
stmt.run(taskId, name, value)
}
/**
* Add a tool call record
* @param taskId Task ID
* @param toolName Tool name
* @param callCount Number of calls
* @param failureCount Number of failures
*/
addToolCall(taskId: string, toolName: string, callCount: number, failureCount: number): void {
const stmt = this.db.prepare(`
INSERT INTO tool_calls (task_id, tool_name, call_count, failure_count)
VALUES (?, ?, ?, ?)
`)
stmt.run(taskId, toolName, callCount, failureCount)
}
/**
* Add a file record
* @param taskId Task ID
* @param filePath File path
* @param status File status (created, modified, deleted)
*/
addFile(taskId: string, filePath: string, status: "created" | "modified" | "deleted"): void {
const stmt = this.db.prepare(`
INSERT INTO files (task_id, path, status)
VALUES (?, ?, ?)
`)
stmt.run(taskId, filePath, status)
}
/**
* Get all runs
* @returns Array of runs
*/
getRuns(): any[] {
const stmt = this.db.prepare(`
SELECT * FROM runs ORDER BY timestamp DESC
`)
return stmt.all()
}
/**
* Get all tasks for a run
* @param runId Run ID
* @returns Array of tasks
*/
getRunTasks(runId: string): any[] {
const stmt = this.db.prepare(`
SELECT * FROM tasks WHERE run_id = ? ORDER BY timestamp ASC
`)
return stmt.all(runId)
}
/**
* Get all metrics for a task
* @param taskId Task ID
* @returns Array of metrics
*/
getTaskMetrics(taskId: string): any[] {
const stmt = this.db.prepare(`
SELECT name, value FROM metrics WHERE task_id = ?
`)
return stmt.all(taskId)
}
/**
* Get all tool calls for a task
* @param taskId Task ID
* @returns Array of tool calls
*/
getTaskToolCalls(taskId: string): any[] {
const stmt = this.db.prepare(`
SELECT tool_name, call_count, failure_count
FROM tool_calls
WHERE task_id = ?
`)
return stmt.all(taskId)
}
/**
* Get all files for a task
* @param taskId Task ID
* @returns Array of files
*/
getTaskFiles(taskId: string): any[] {
const stmt = this.db.prepare(`
SELECT path, status FROM files WHERE task_id = ?
`)
return stmt.all(taskId)
}
/**
* Close the database connection
*/
close(): void {
this.db.close()
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
* SQL schema for the evaluation database
*/
export const SCHEMA = `
CREATE TABLE IF NOT EXISTS runs (
id TEXT PRIMARY KEY,
timestamp INTEGER NOT NULL,
model TEXT NOT NULL,
benchmark TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
success INTEGER NOT NULL DEFAULT 0,
total_tool_calls INTEGER DEFAULT 0,
total_tool_failures INTEGER DEFAULT 0,
FOREIGN KEY (run_id) REFERENCES runs(id)
);
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
name TEXT NOT NULL,
value REAL NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id)
);
CREATE TABLE IF NOT EXISTS tool_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
tool_name TEXT NOT NULL,
call_count INTEGER NOT NULL,
failure_count INTEGER NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id)
);
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
path TEXT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id)
);
`
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env node
import { Command } from "commander"
import chalk from "chalk"
import { setupHandler } from "./commands/setup"
import { runHandler } from "./commands/run"
import { reportHandler } from "./commands/report"
// Create the CLI program
const program = new Command()
// Set up CLI metadata
program.name("cline-eval").description("CLI tool for orchestrating Cline evaluations across multiple benchmarks").version("0.1.0")
// Setup command
program
.command("setup")
.description("Clone and set up benchmark repositories")
.option(
"-b, --benchmarks <benchmarks>",
"Comma-separated list of benchmarks to set up",
"exercism,swe-bench,swelancer,multi-swe",
)
.action(async (options) => {
try {
await setupHandler(options)
} catch (error) {
console.error(chalk.red(`Error during setup: ${error instanceof Error ? error.message : String(error)}`))
process.exit(1)
}
})
// Run command
program
.command("run")
.description("Run evaluations")
.option("-b, --benchmark <benchmark>", "Specific benchmark to run")
.option("-m, --model <model>", "Model to evaluate", "claude-3-opus-20240229")
.option("-c, --count <count>", "Number of tasks to run", parseInt)
.option("-k, --api-key <apiKey>", "Cline API key to use for evaluations")
.action(async (options) => {
try {
await runHandler(options)
} catch (error) {
console.error(chalk.red(`Error during run: ${error instanceof Error ? error.message : String(error)}`))
process.exit(1)
}
})
// Report command
program
.command("report")
.description("Generate reports")
.option("-f, --format <format>", "Report format (json, markdown)", "markdown")
.option("-o, --output <path>", "Output path for the report")
.action(async (options) => {
try {
await reportHandler(options)
} catch (error) {
console.error(chalk.red(`Error generating report: ${error instanceof Error ? error.message : String(error)}`))
process.exit(1)
}
})
// Parse command line arguments
program.parse(process.argv)
// If no arguments provided, show help
if (process.argv.length === 2) {
program.help()
}
+131
View File
@@ -0,0 +1,131 @@
import execa from "execa"
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
/**
* List of VSCode extensions to install for evaluation environments
* These extensions provide language support and other useful features
*/
export const REQUIRED_EXTENSIONS = [
"golang.go", // Go language support
"dbaeumer.vscode-eslint", // ESLint support
"redhat.java", // Java support
"ms-python.python", // Python support
"rust-lang.rust-analyzer", // Rust support
"ms-vscode.cpptools", // C/C++ support
]
/**
* Install required VSCode extensions in the specified extensions directory
* @param extensionsDir The directory where extensions should be installed
* @returns Promise that resolves when all extensions are installed
*/
export async function installRequiredExtensions(extensionsDir: string): Promise<void> {
console.log("Installing required VSCode extensions...")
// Create the extensions directory if it doesn't exist
if (!fs.existsSync(extensionsDir)) {
fs.mkdirSync(extensionsDir, { recursive: true })
}
// Install each extension
for (const extension of REQUIRED_EXTENSIONS) {
try {
console.log(`Installing extension: ${extension}...`)
await execa("code", ["--extensions-dir", extensionsDir, "--install-extension", extension, "--force"])
console.log(`✅ Extension ${extension} installed successfully`)
} catch (error: any) {
console.warn(`⚠️ Failed to install extension ${extension}: ${error.message}`)
// Continue with other extensions even if one fails
}
}
console.log("✅ All required extensions installed")
}
/**
* Check if a VSCode extension is installed in the specified directory
* @param extensionsDir The directory to check for installed extensions
* @param extensionId The ID of the extension to check
* @returns True if the extension is installed, false otherwise
*/
export function isExtensionInstalled(extensionsDir: string, extensionId: string): boolean {
// Extensions are installed in directories named publisher.name-version
// We need to check if any directory starts with the extensionId
const extensionPrefix = extensionId.toLowerCase() + "-"
try {
const files = fs.readdirSync(extensionsDir)
return files.some((file) => {
const lowerCaseFile = file.toLowerCase()
return lowerCaseFile === extensionId.toLowerCase() || lowerCaseFile.startsWith(extensionPrefix)
})
} catch (error) {
return false
}
}
/**
* Get the path to the VSCode settings file in the specified user data directory
* @param userDataDir The VSCode user data directory
* @returns The path to the settings.json file
*/
export function getSettingsPath(userDataDir: string): string {
const settingsDir = path.join(userDataDir, "User")
fs.mkdirSync(settingsDir, { recursive: true })
return path.join(settingsDir, "settings.json")
}
/**
* Configure extension settings in the VSCode user data directory
* @param userDataDir The VSCode user data directory
*/
export function configureExtensionSettings(userDataDir: string): void {
const settingsPath = getSettingsPath(userDataDir)
// Read existing settings if they exist
let settings = {}
if (fs.existsSync(settingsPath)) {
try {
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
} catch (error) {
console.warn(`Error reading settings file: ${error}`)
}
}
// Add or update extension-specific settings
const updatedSettings = {
...settings,
// Go extension settings
"go.toolsManagement.autoUpdate": false,
"go.survey.prompt": false,
// ESLint settings
"eslint.enable": true,
"eslint.run": "onSave",
// Java settings
"java.configuration.checkProjectSettingsExclusions": false,
"java.configure.checkForOutdatedExtensions": false,
"java.help.firstView": false,
// Python settings
"python.experiments.enabled": false,
"python.showStartPage": false,
// Rust settings
"rust-analyzer.checkOnSave.command": "check",
// C/C++ settings
"C_Cpp.intelliSenseEngine": "default",
// General extension settings
"extensions.autoUpdate": false,
"extensions.ignoreRecommendations": true,
}
// Write updated settings
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2))
console.log("✅ Extension settings configured")
}
+109
View File
@@ -0,0 +1,109 @@
import * as fs from "fs"
import * as path from "path"
/**
* Generate a markdown report from evaluation results
* @param summary Overall summary
* @param benchmarkReports Benchmark-specific reports
* @param modelReports Model-specific reports
* @param outputPath Output file path
*/
export function generateMarkdownReport(
summary: any,
benchmarkReports: Record<string, any>,
modelReports: Record<string, any>,
outputPath: string,
): void {
let markdown = `# Cline Evaluation Report\n\n`
// Generate summary section
markdown += `## Summary\n\n`
markdown += `- **Total Runs:** ${summary.runs}\n`
markdown += `- **Models:** ${summary.models.join(", ")}\n`
markdown += `- **Benchmarks:** ${summary.benchmarks.join(", ")}\n`
markdown += `- **Total Tasks:** ${summary.tasks}\n`
markdown += `- **Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
markdown += `- **Average Tokens:** ${Math.round(summary.averageTokens)}\n`
markdown += `- **Average Cost:** $${summary.averageCost.toFixed(4)}\n`
markdown += `- **Average Duration:** ${(summary.averageDuration / 1000).toFixed(2)}s\n`
markdown += `- **Total Tool Calls:** ${summary.totalToolCalls}\n`
markdown += `- **Tool Success Rate:** ${(summary.toolSuccessRate * 100).toFixed(2)}%\n\n`
// Generate tool usage section
markdown += `## Tool Usage\n\n`
markdown += `| Tool | Calls | Failures | Success Rate |\n`
markdown += `| ---- | ----- | -------- | ------------ |\n`
for (const [toolName, metrics] of Object.entries(summary.toolUsage)) {
const calls = (metrics as any).calls
const failures = (metrics as any).failures
const successRate = calls > 0 ? (1 - failures / calls) * 100 : 100
markdown += `| ${toolName} | ${calls} | ${failures} | ${successRate.toFixed(2)}% |\n`
}
// Generate benchmark results section
markdown += `\n## Benchmark Results\n\n`
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
markdown += `### ${benchmark}\n\n`
markdown += `- **Runs:** ${report.runs}\n`
markdown += `- **Models:** ${report.models.join(", ")}\n`
markdown += `- **Tasks:** ${report.tasks}\n`
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
}
// Generate model results section
markdown += `## Model Results\n\n`
for (const [model, report] of Object.entries(modelReports)) {
markdown += `### ${model}\n\n`
markdown += `- **Runs:** ${report.runs}\n`
markdown += `- **Benchmarks:** ${report.benchmarks.join(", ")}\n`
markdown += `- **Tasks:** ${report.tasks}\n`
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
}
// Add charts using Mermaid
markdown += `## Charts\n\n`
// Success rate by benchmark chart
markdown += `### Success Rate by Benchmark\n\n`
markdown += "```mermaid\n"
markdown += "graph TD\n"
markdown += " title[Success Rate by Benchmark]\n"
markdown += " style title fill:none,stroke:none\n\n"
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
const successRate = (report.successRate * 100).toFixed(2)
markdown += ` ${benchmark}[${benchmark}: ${successRate}%]\n`
}
markdown += "```\n\n"
// Success rate by model chart
markdown += `### Success Rate by Model\n\n`
markdown += "```mermaid\n"
markdown += "graph TD\n"
markdown += " title[Success Rate by Model]\n"
markdown += " style title fill:none,stroke:none\n\n"
for (const [model, report] of Object.entries(modelReports)) {
const successRate = (report.successRate * 100).toFixed(2)
markdown += ` ${model.replace(/[-\.]/g, "_")}[${model}: ${successRate}%]\n`
}
markdown += "```\n\n"
// Add timestamp
markdown += `\n\n---\n\nReport generated on ${new Date().toISOString()}\n`
// Write markdown to file
fs.writeFileSync(outputPath, markdown)
}
+79
View File
@@ -0,0 +1,79 @@
import { v4 as uuidv4 } from "uuid"
import { ResultsDatabase } from "../db"
import { Task } from "../adapters/types"
/**
* Store task result in the database
* @param runId The run ID
* @param task The task that was executed
* @param result The result from the test server
* @param verification The verification result
*/
export async function storeTaskResult(runId: string, task: Task, result: any, verification: any): Promise<void> {
const db = new ResultsDatabase()
const taskId = uuidv4()
try {
// Extract metrics from the result
const { metrics } = result
const totalToolCalls = metrics?.totalToolCalls || 0
const totalToolFailures = metrics?.totalToolFailures || 0
// Create task with tool metrics
db.createTask(taskId, runId, task.id)
db.completeTask(taskId, verification.success, totalToolCalls, totalToolFailures)
// Store metrics
if (metrics) {
// Store token metrics
if (metrics.tokensIn) db.addMetric(taskId, "tokensIn", metrics.tokensIn)
if (metrics.tokensOut) db.addMetric(taskId, "tokensOut", metrics.tokensOut)
if (metrics.cost) db.addMetric(taskId, "cost", metrics.cost)
if (metrics.duration) db.addMetric(taskId, "duration", metrics.duration)
// Store tool call metrics
if (metrics.toolCalls) {
for (const [toolName, callCount] of Object.entries(metrics.toolCalls)) {
const failureCount = metrics.toolFailures?.[toolName] || 0
db.addToolCall(taskId, toolName, callCount as number, failureCount)
}
}
}
// Store verification metrics
if (verification.metrics) {
for (const [key, value] of Object.entries(verification.metrics)) {
if (typeof value === "number") {
db.addMetric(taskId, key, value)
}
}
}
// Store file changes
if (result.files) {
// Store created files
if (result.files.created) {
for (const file of result.files.created) {
db.addFile(taskId, file, "created")
}
}
// Store modified files
if (result.files.modified) {
for (const file of result.files.modified) {
db.addFile(taskId, file, "modified")
}
}
// Store deleted files
if (result.files.deleted) {
for (const file of result.files.deleted) {
db.addFile(taskId, file, "deleted")
}
}
}
} finally {
// Close the database connection
db.close()
}
}
+52
View File
@@ -0,0 +1,52 @@
import fetch from "node-fetch"
import chalk from "chalk"
/**
* Send a task to the Cline test server
* @param task The task description to send
* @param apiKey Optional Cline API key to use for the task
* @returns The result of the task execution
*/
export async function sendTaskToServer(task: string, apiKey?: string): Promise<any> {
const SERVER_URL = "http://localhost:9876/task"
try {
console.log(chalk.blue(`Sending task to server: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}`))
const response = await fetch(SERVER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
task,
apiKey,
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
}
const result = await response.json()
if (!result.success) {
throw new Error(`Task execution failed: ${result.error || "Unknown error"}`)
}
if (result.timeout) {
throw new Error("Task execution timed out")
}
return result
} catch (error: any) {
if (error.code === "ECONNREFUSED") {
throw new Error(
"Could not connect to the test server. Make sure VSCode is running with the Cline extension and the test server is active.",
)
}
throw error
}
}
+570
View File
@@ -0,0 +1,570 @@
import execa from "execa"
import * as path from "path"
import * as fs from "fs"
import fetch from "node-fetch"
import * as os from "os"
import * as child_process from "child_process"
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
// Store temporary directories for cleanup
interface VSCodeResources {
tempUserDataDir: string
tempExtensionsDir: string
vscodePid?: number
}
// Global map to track resources for each workspace
const workspaceResources = new Map<string, VSCodeResources>()
/**
* Spawn a VSCode instance with the Cline extension
* @param workspacePath The workspace path to open
* @param vsixPath Optional path to a VSIX file to install
* @returns The resources created for this VS Code instance
*/
export async function spawnVSCode(workspacePath: string, vsixPath?: string): Promise<VSCodeResources> {
// Ensure the workspace path exists
if (!fs.existsSync(workspacePath)) {
throw new Error(`Workspace path does not exist: ${workspacePath}`)
}
// If no VSIX path is provided, build one with IS_TEST=true
if (!vsixPath) {
try {
// Build the VSIX with IS_TEST=true
console.log("Building test VSIX...")
const clineRoot = path.resolve(process.cwd(), "..", "..")
await execa("npx", ["vsce", "package"], {
cwd: clineRoot,
env: {
IS_TEST: "true",
},
stdio: "inherit",
})
// Find the generated VSIX file
const files = fs.readdirSync(clineRoot)
const vsixFile = files.find((file) => file.endsWith(".vsix"))
if (vsixFile) {
vsixPath = path.join(clineRoot, vsixFile)
console.log(`Using built VSIX: ${vsixPath}`)
} else {
console.warn("Could not find generated VSIX file")
}
} catch (error) {
console.warn("Failed to build test VSIX:", error)
}
}
// Create a temporary user data directory for this VS Code instance
const tempUserDataDir = path.join(os.tmpdir(), `vscode-cline-eval-${Date.now()}`)
fs.mkdirSync(tempUserDataDir, { recursive: true })
console.log(`Created temporary user data directory: ${tempUserDataDir}`)
// Create a temporary extensions directory to ensure no other extensions are loaded
const tempExtensionsDir = path.join(os.tmpdir(), `vscode-cline-eval-ext-${Date.now()}`)
fs.mkdirSync(tempExtensionsDir, { recursive: true })
console.log(`Created temporary extensions directory: ${tempExtensionsDir}`)
// Create settings.json in the temporary user data directory to disable workspace trust
// and configure Cline to auto-open on startup
const settingsDir = path.join(tempUserDataDir, "User")
fs.mkdirSync(settingsDir, { recursive: true })
const settingsPath = path.join(settingsDir, "settings.json")
const settings = {
// Disable workspace trust
"security.workspace.trust.enabled": false,
"security.workspace.trust.startupPrompt": "never",
"security.workspace.trust.banner": "never",
"security.workspace.trust.emptyWindow": true,
// Configure startup behavior
"workbench.startupEditor": "none",
// Auto-open Cline on startup
"cline.autoOpenOnStartup": true,
// Show the activity bar and sidebar
"workbench.activityBar.visible": true,
"workbench.sideBar.visible": true,
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.visible": true,
"workbench.view.alwaysShowHeaderActions": true,
"workbench.editor.openSideBySideDirection": "right",
// Disable GitLens from opening automatically
"gitlens.views.repositories.autoReveal": false,
"gitlens.views.fileHistory.autoReveal": false,
"gitlens.views.lineHistory.autoReveal": false,
"gitlens.views.compare.autoReveal": false,
"gitlens.views.search.autoReveal": false,
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
// Disable other extensions that might compete for startup focus
"extensions.autoUpdate": false,
}
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
console.log(`Created settings.json to disable workspace trust and auto-open Cline`)
// Create keybindings.json to automatically open Cline on startup
const keybindingsPath = path.join(settingsDir, "keybindings.json")
const keybindings = [
{
key: "alt+c",
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
},
{
key: "alt+shift+c",
command: "cline.openInNewTab",
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
},
]
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
console.log(`Created keybindings.json to help with Cline activation`)
// Build the command arguments with custom user data directory
const args = [
// Use a custom user data directory to isolate this instance
"--user-data-dir",
tempUserDataDir,
// Use a custom extensions directory to ensure only our extension is loaded
"--extensions-dir",
tempExtensionsDir,
// Disable workspace trust
"--disable-workspace-trust",
"-n",
workspacePath,
// Force the extension to be activated on startup
"--start-up-extension",
"saoudrizwan.claude-dev",
// Run a command on startup to open Cline
"--command",
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
// Additional flags to help with extension activation
"--disable-gpu=false",
"--max-memory=4096",
]
// Create a startup script to run commands after VS Code launches
const startupScriptPath = path.join(settingsDir, "startup.js")
const startupScript = `
// This script will be executed when VS Code starts
setTimeout(() => {
// Try to open Cline in the sidebar
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
// Also try to open Cline in a tab as a fallback
setTimeout(() => {
require('vscode').commands.executeCommand('cline.openInNewTab');
}, 5000);
}, 5000);
`
fs.writeFileSync(startupScriptPath, startupScript)
console.log(`Created startup script to activate Cline`)
// If a VSIX is provided, install it
if (vsixPath) {
if (!fs.existsSync(vsixPath)) {
throw new Error(`VSIX file does not exist: ${vsixPath}`)
}
args.unshift("--install-extension", vsixPath)
}
// Install required extensions
console.log("Installing required VSCode extensions...")
await installRequiredExtensions(tempExtensionsDir)
// Configure extension settings
console.log("Configuring extension settings...")
configureExtensionSettings(tempUserDataDir)
// Execute the command
try {
// We don't need to install extensions globally anymore since we're using a custom user data directory
// The VSIX will be installed in the isolated environment if provided in the args
// Launch VS Code
console.log("Launching VS Code...")
await execa("code", args, {
stdio: "inherit",
})
// Wait longer for VSCode to initialize and extension to load
console.log("Waiting for VS Code to initialize...")
await new Promise((resolve) => setTimeout(resolve, 30000))
// Create a JavaScript file that will be loaded as a VS Code extension
const extensionDir = path.join(tempExtensionsDir, "cline-activator")
fs.mkdirSync(extensionDir, { recursive: true })
// Create package.json for the extension
const packageJsonPath = path.join(extensionDir, "package.json")
const packageJson = {
name: "cline-activator",
displayName: "Cline Activator",
description: "Activates Cline and starts the test server",
version: "0.0.1",
engines: {
vscode: "^1.60.0",
},
main: "./extension.js",
activationEvents: ["*"],
contributes: {
commands: [
{
command: "cline-activator.activate",
title: "Activate Cline",
},
],
},
}
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
// Create extension.js
const extensionJsPath = path.join(extensionDir, "extension.js")
const extensionJs = `
const vscode = require('vscode');
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
console.log('Cline Activator is now active!');
// Register the command to activate Cline
let disposable = vscode.commands.registerCommand('cline-activator.activate', async function () {
try {
// Make sure the Cline extension is activated
const extension = vscode.extensions.getExtension('saoudrizwan.claude-dev');
if (!extension) {
console.error('Cline extension not found');
return;
}
if (!extension.isActive) {
console.log('Activating Cline extension...');
await extension.activate();
}
// Show the Cline sidebar
console.log('Opening Cline sidebar...');
await vscode.commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
// Wait a moment for the sidebar to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Also open Cline in a tab as a fallback
console.log('Opening Cline in a tab...');
await vscode.commands.executeCommand('cline.openInNewTab');
// Wait a moment for the tab to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Create the test server if it doesn't exist
console.log('Creating test server...');
// Get the visible webview instance
const clineRootPath = '${path.resolve(process.cwd(), "..", "..")}';
const visibleWebview = require(path.join(clineRootPath, 'src', 'core', 'webview')).WebviewProvider.getVisibleInstance();
if (visibleWebview) {
require(path.join(clineRootPath, 'src', 'services', 'test', 'TestServer')).createTestServer(visibleWebview);
console.log('Test server created successfully');
} else {
console.error('No visible webview instance found');
}
} catch (error) {
console.error('Error activating Cline:', error);
}
});
context.subscriptions.push(disposable);
// Automatically run the command after a delay
setTimeout(() => {
vscode.commands.executeCommand('cline-activator.activate');
}, 5000);
}
function deactivate() {}
module.exports = {
activate,
deactivate
}
`
fs.writeFileSync(extensionJsPath, extensionJs)
console.log(`Created Cline Activator extension`)
// Try multiple approaches to activate the extension
let serverStarted = false
// Create an activation script to run in VS Code
const activationScriptPath = path.join(settingsDir, "activate-cline.js")
const activationScript = `
// This script will be executed to activate Cline and start the test server
const vscode = require('vscode');
// Execute the cline-activator.activate command
vscode.commands.executeCommand('cline-activator.activate');
`
fs.writeFileSync(activationScriptPath, activationScript)
console.log(`Created activation script to run in VS Code`)
// Execute the activation script
try {
console.log("Executing activation script to start Cline and test server...")
await execa(
"code",
[
"--user-data-dir",
tempUserDataDir,
"--extensions-dir",
tempExtensionsDir,
"--folder-uri",
`file://${workspacePath}`,
"--execute",
activationScriptPath,
],
{
stdio: "inherit",
},
)
// Wait for the test server to start
console.log("Waiting for test server to start...")
for (let i = 0; i < 30; i++) {
try {
// Try to connect to the test server
const response = await fetch("http://localhost:9876/task", {
method: "OPTIONS",
headers: {
"Content-Type": "application/json",
},
})
if (response.status === 204) {
console.log("Test server is running!")
serverStarted = true
break
}
} catch (error) {
// Server not started yet, wait and try again
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
} catch (error) {
console.warn("Failed to execute activation script:", error)
}
if (!serverStarted) {
console.warn("Test server did not start after multiple attempts")
console.log("You may need to manually open the Cline extension in VS Code")
}
// Store the resources for this workspace
const resources: VSCodeResources = {
tempUserDataDir,
tempExtensionsDir,
}
// Store in the global map
workspaceResources.set(workspacePath, resources)
// Return the resources
return resources
} catch (error: any) {
throw new Error(`Failed to spawn VSCode: ${error.message}`)
}
}
/**
* Clean up VS Code resources and shut down the test server
* @param workspacePath The workspace path to clean up resources for
*/
export async function cleanupVSCode(workspacePath: string): Promise<void> {
console.log(`Cleaning up VS Code resources for workspace: ${workspacePath}`)
// Get the resources for this workspace
const resources = workspaceResources.get(workspacePath)
if (!resources) {
console.log(`No resources found for workspace: ${workspacePath}`)
return
}
// Try to shut down the test server
try {
console.log("Shutting down test server...")
await fetch("http://localhost:9876/shutdown", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
}).catch(() => {
// Ignore errors, the server might already be down
})
} catch (error) {
console.warn(`Error shutting down test server: ${error}`)
}
// Try to gracefully close VS Code instead of killing it
try {
console.log("Attempting to gracefully close VS Code...")
// Create a settings file that will disable the crash reporter and the exit confirmation dialog
const settingsDir = path.join(resources.tempUserDataDir, "User")
const settingsPath = path.join(settingsDir, "settings.json")
// Read existing settings if they exist
let settings = {}
if (fs.existsSync(settingsPath)) {
try {
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
} catch (error) {
console.warn(`Error reading settings file: ${error}`)
}
}
// Update settings to disable crash reporter and exit confirmation
settings = {
...settings,
"window.confirmBeforeClose": "never",
"telemetry.enableCrashReporter": false,
"window.restoreWindows": "none",
"window.newWindowDimensions": "default",
}
// Write updated settings
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
// On macOS, use AppleScript to quit VS Code gracefully
if (process.platform === "darwin") {
try {
// First try AppleScript to quit VS Code gracefully
await execa("osascript", ["-e", 'tell application "Visual Studio Code" to quit'])
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (appleScriptError) {
console.warn(`Error using AppleScript to quit VS Code: ${appleScriptError}`)
}
} else if (process.platform === "win32") {
// On Windows, try to use taskkill without /F first
try {
await execa("taskkill", ["/IM", "code.exe"])
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (taskkillError) {
console.warn(`Error using taskkill to quit VS Code: ${taskkillError}`)
}
} else {
// On Linux, try to use SIGTERM first
try {
// Find VS Code processes
const { stdout } = await execa("ps", ["aux"])
const lines = stdout.split("\n")
for (const line of lines) {
if (line.includes(resources.tempUserDataDir)) {
const parts = line.trim().split(/\s+/)
const pid = parseInt(parts[1])
if (pid && !isNaN(pid)) {
console.log(`Sending SIGTERM to VS Code process with PID: ${pid}`)
try {
// Use SIGTERM instead of SIGKILL for a graceful shutdown
process.kill(pid, "SIGTERM")
} catch (killError) {
console.warn(`Failed to terminate process ${pid}: ${killError}`)
}
}
}
}
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (psError) {
console.warn(`Error listing processes: ${psError}`)
}
}
// If graceful methods failed, fall back to forceful termination as a last resort
// Check if VS Code is still running with the temp user data dir
let vsCodeStillRunning = false
if (process.platform !== "win32") {
try {
const { stdout } = await execa("ps", ["aux"])
vsCodeStillRunning = stdout.split("\n").some((line) => line.includes(resources.tempUserDataDir))
} catch (error) {
console.warn(`Error checking if VS Code is still running: ${error}`)
}
} else {
try {
const { stdout } = await execa("tasklist", ["/FI", `IMAGENAME eq code.exe`])
vsCodeStillRunning = stdout.includes("code.exe")
} catch (error) {
console.warn(`Error checking if VS Code is still running: ${error}`)
}
}
// If VS Code is still running, use forceful termination as a last resort
if (vsCodeStillRunning) {
console.log("Graceful shutdown failed, falling back to forceful termination...")
if (process.platform === "win32") {
try {
await execa("taskkill", ["/IM", "code.exe", "/F"])
} catch (error) {
console.warn(`Error forcefully terminating VS Code: ${error}`)
}
} else {
try {
const { stdout } = await execa("ps", ["aux"])
const lines = stdout.split("\n")
for (const line of lines) {
if (line.includes(resources.tempUserDataDir)) {
const parts = line.trim().split(/\s+/)
const pid = parseInt(parts[1])
if (pid && !isNaN(pid)) {
console.log(`Forcefully killing VS Code process with PID: ${pid}`)
try {
process.kill(pid, "SIGKILL")
} catch (killError) {
console.warn(`Failed to kill process ${pid}: ${killError}`)
}
}
}
}
} catch (error) {
console.warn(`Error forcefully terminating VS Code: ${error}`)
}
}
}
} catch (error) {
console.warn(`Error closing VS Code: ${error}`)
}
// Clean up temporary directories
try {
console.log(`Removing temporary user data directory: ${resources.tempUserDataDir}`)
fs.rmSync(resources.tempUserDataDir, { recursive: true, force: true })
} catch (error) {
console.warn(`Error removing temporary user data directory: ${error}`)
}
try {
console.log(`Removing temporary extensions directory: ${resources.tempExtensionsDir}`)
fs.rmSync(resources.tempExtensionsDir, { recursive: true, force: true })
} catch (error) {
console.warn(`Error removing temporary extensions directory: ${error}`)
}
// Remove from the global map
workspaceResources.delete(workspacePath)
console.log("Cleanup completed")
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+164 -36
View File
@@ -1,20 +1,22 @@
{
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
@@ -79,15 +81,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"engines": {
@@ -3957,6 +3962,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
"integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@changesets/apply-release-plan": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.8.tgz",
@@ -5566,6 +5577,7 @@
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
"integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -9919,6 +9931,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/case-anything": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
@@ -9938,35 +9963,18 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@@ -10612,6 +10620,19 @@
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz",
@@ -10722,6 +10743,16 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dprint-node": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
@@ -11192,6 +11223,23 @@
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -11240,6 +11288,19 @@
"node": ">=8"
}
},
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -13659,6 +13720,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/log-symbols/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
@@ -14768,19 +14859,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ora/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
@@ -15366,6 +15444,20 @@
"node": ">=12.0.0"
}
},
"node_modules/protoc-gen-ts": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/protoc-gen-ts/-/protoc-gen-ts-0.8.7.tgz",
"integrity": "sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==",
"dev": true,
"license": "MIT",
"bin": {
"protoc-gen-ts": "protoc-gen-ts.js"
},
"funding": {
"type": "individual",
"url": "https://www.buymeacoffee.com/thesayyn"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -16970,6 +17062,42 @@
"node": ">=0.3.1"
}
},
"node_modules/ts-poet": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.11.0.tgz",
"integrity": "sha512-r5AGF8vvb+GjBsnqiTqbLhN1/U2FJt6BI+k0dfCrkKzWvUhNlwMmq9nDHuucHs45LomgHjZPvYj96dD3JawjJA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dprint-node": "^1.0.8"
}
},
"node_modules/ts-proto": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.0.tgz",
"integrity": "sha512-BGHjse2wTOeswOqnnPKinpxmbaRd882so/e1En6ww59YMG7AO9Kg4vPpJcbVfrpBixPRDqHafXD/RDyd2T99GA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"case-anything": "^2.1.13",
"ts-poet": "^6.7.0",
"ts-proto-descriptors": "2.0.0"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz",
"integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+12 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.12.2",
"version": "3.12.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -116,6 +116,11 @@
"command": "cline.fixWithCline",
"title": "Fix with Cline",
"category": "Cline"
},
{
"command": "cline.focusChatInput",
"title": "Jump to Chat Input",
"category": "Cline"
}
],
"keybindings": [
@@ -290,6 +295,7 @@
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
@@ -334,15 +340,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"dependencies": {
@@ -350,8 +359,10 @@
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package cline;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
}
message BrowserConnectionInfo {
bool is_connected = 1;
bool is_remote = 2;
optional string host = 3;
}
message BrowserConnection {
bool success = 1;
string message = 2;
optional string endpoint = 3;
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env node
import * as fs from "fs/promises"
import * as path from "path"
import { execSync } from "child_process"
import { globby } from "globby"
import chalk from "chalk"
// Get script directory and root directory
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
// Check if protoc is installed
try {
const options = { stdio: "ignore" }
execSync("protoc --version", options)
} catch (error) {
console.warn(chalk.yellow("Warning: protoc is not installed. Skipping proto generation."))
console.warn(chalk.yellow("To install Protocol Buffers compiler, visit: https://grpc.io/docs/protoc-installation/"))
process.exit(0) // Exit with success as requested
}
// Check if ts-proto plugin is available
const TS_PROTO_PLUGIN = path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto")
try {
await fs.access(TS_PROTO_PLUGIN)
} catch (error) {
console.error(chalk.red("Error: ts-proto plugin not found at"), TS_PROTO_PLUGIN)
console.error(chalk.red('Please run "npm install" to install the required dependencies.'))
process.exit(1)
}
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directory if it doesn't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR })
for (const protoFile of protoFiles) {
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
// Build the protoc command with proper path handling for cross-platform
const protocCommand = [
"protoc",
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
`--proto_path="${SCRIPT_DIR}"`,
`"${path.join(SCRIPT_DIR, protoFile)}"`,
].join(" ")
try {
const execOptions = {
stdio: "inherit",
}
execSync(protocCommand, execOptions)
} catch (error) {
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
process.exit(1)
}
}
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
// Generate method registration files
await generateMethodRegistrations()
// Make the script executable
try {
await fs.chmod(path.join(SCRIPT_DIR, "build-proto.js"), 0o755)
} catch (error) {
console.warn(chalk.yellow("Warning: Could not make script executable:"), error)
}
}
async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
// Add more service directories here as needed
]
for (const serviceDir of serviceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.gray(`Skipping ${serviceDir} - directory does not exist`))
continue
}
const serviceName = path.basename(serviceDir)
const registryFile = path.join(serviceDir, "methods.ts")
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the output file with header
let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"\n`
// Add imports for all implementation files
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
content += `import { ${baseName} } from "./${baseName}"\n`
}
// Add registration function
content += `\n// Register all ${serviceName} service methods
export function registerAllMethods(): void {
\t// Register each method with the registry\n`
// Add registration statements
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
content += `\tregisterMethod("${baseName}", ${baseName})\n`
}
// Close the function
content += `}`
// Write the file
await fs.writeFile(registryFile, content)
console.log(chalk.green(`Generated ${registryFile}`))
}
console.log(chalk.green("Method registration files generated successfully."))
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
+40
View File
@@ -0,0 +1,40 @@
syntax = "proto3";
package cline;
message Metadata {
}
message EmptyRequest {
Metadata metadata = 1;
}
message Empty {
}
message StringRequest {
Metadata metadata = 1;
string value = 2;
}
message String {
string value = 1;
}
message Int64Request {
Metadata metadata = 1;
int64 value = 2;
}
message Int64 {
int64 value = 1;
}
message BytesRequest {
Metadata metadata = 1;
bytes value = 2;
}
message Bytes {
bytes value = 1;
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+3 -1
View File
@@ -50,7 +50,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1":
case "o1-preview":
case "o1-mini": {
// o1 doesnt support streaming, non-1 temp, or system prompt
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
@@ -64,6 +64,8 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
model: model.id,
+2 -2
View File
@@ -35,7 +35,7 @@ export class OpenAiHandler implements ApiHandler {
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
const isO3Mini = modelId.includes("o3-mini")
const isReasoningModelFamily = modelId.includes("o3") || modelId.includes("o4")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -55,7 +55,7 @@ export class OpenAiHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
if (isO3Mini) {
if (isReasoningModelFamily) {
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
temperature = undefined // does not support temperature
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
+12
View File
@@ -5,6 +5,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { VertexAI } from "@google-cloud/vertexai"
import { calculateApiCostOpenAI } from "../../utils/cost"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler {
@@ -266,6 +267,17 @@ export class VertexHandler implements ApiHandler {
}
}
}
// Handle token usage metadata
const { usageMetadata } = await streamingResult.response
if (usageMetadata) {
const { promptTokenCount = 0, candidatesTokenCount = 0 } = usageMetadata
yield {
type: "usage",
inputTokens: promptTokenCount,
outputTokens: candidatesTokenCount,
totalCost: calculateApiCostOpenAI(model.info, promptTokenCount, candidatesTokenCount, 0, 0),
}
}
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -47,7 +47,7 @@ export class XAIHandler implements ApiHandler {
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
@@ -123,7 +123,7 @@ export function parseAssistantMessage(assistantMessage: string) {
contentBlocks.push(currentToolUse)
}
// Note: it doesnt matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
if (currentTextContent) {
// stream did not complete text content, add it as partial
contentBlocks.push(currentTextContent)
@@ -537,7 +537,7 @@ export class ContextManager {
// we can assume that thisExistingFileReads does not have many entries
if (!thisExistingFileReads.includes(filePath)) {
// meaning we havent already replaced this file read
// meaning we haven't already replaced this file read
const entireMatch = match[0] // The entire matched string
@@ -590,7 +590,7 @@ export class ContextManager {
) {
const pattern = new RegExp(`(<final_file_content path="[^"]*">)[\\s\\S]*?(</final_file_content>)`)
// check if this exists in the text, it wont exist if the user rejects the file change for example
// check if this exists in the text, it won't exist if the user rejects the file change for example
if (pattern.test(secondBlockText)) {
const replacementText = secondBlockText.replace(pattern, `$1 ${formatResponse.duplicateFileReadNotice()} $2`)
const indices = fileReadIndices.get(filePath) || []
@@ -741,7 +741,7 @@ export class ContextManager {
let totalCharactersSaved = 0
for (let i = startIndex; i < endIndex; i++) {
// looping over the outer indicies of messages
// looping over the outer indices of messages
const message = apiMessages[i]
if (!message.content) {
@@ -782,7 +782,7 @@ export class ContextManager {
totalCharCount += originalTextLength
} else {
// meaning there was an update to this text previously, but we didnt just alter it
// meaning there was an update to this text previously, but we didn't just alter it
totalCharCount += latestUpdate[2][0].length
}
} else {
@@ -790,7 +790,7 @@ export class ContextManager {
totalCharCount += block.text.length
}
} else {
// reach here if there's no alterations for this outer index, meaning each inner index wont have any changes either
// reach here if there's no alterations for this outer index, meaning each inner index won't have any changes either
totalCharCount += block.text.length
}
} else if (block.type === "image" && block.source) {
@@ -1,10 +1,42 @@
import path from "path"
import { GlobalFileNames } from "../../../storage/disk"
import { ensureRulesDirectoryExists, GlobalFileNames } from "../../../storage/disk"
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
import { formatResponse } from "../../../prompts/responses"
import fs from "fs/promises"
import { ClineRulesToggles } from "../../../../shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../storage/state"
import * as vscode from "vscode"
export const getClineRules = async (cwd: string) => {
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(
rulesFilePaths,
globalClineRulesFilePath,
toggles,
)
if (rulesFilesTotalContent) {
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(
globalClineRulesFilePath,
rulesFilesTotalContent,
)
return clineRulesFileInstructions
}
} catch {
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
} else {
console.error(`${globalClineRulesFilePath} is not a directory`)
return undefined
}
}
return undefined
}
export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles) => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
@@ -12,17 +44,21 @@ export const getClineRules = async (cwd: string) => {
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
clineRulesFileInstructions = formatResponse.clineRulesDirectoryInstructions(cwd, rulesFilesTotalContent)
const rulesFilePaths = await readDirectory(clineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesFileInstructions(cwd, ruleFileContent)
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
@@ -33,13 +69,107 @@ export const getClineRules = async (cwd: string) => {
return clineRulesFileInstructions
}
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], cwd: string) => {
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(cwd, filePath)
const ruleFilePathRelative = path.relative(cwd, ruleFilePath)
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
return null
}
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.join("\n\n"))
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
try {
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
if (pathExists) {
const isDir = await isDirectory(rulesDirectoryPath)
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectory(rulesDirectoryPath)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
existingRulePaths.add(ruleFilePath)
const pathHasToggle = ruleFilePath in updatedToggles
if (!pathHasToggle) {
updatedToggles[ruleFilePath] = true
}
}
// Clean up toggles for non-existent files
for (const togglePath in updatedToggles) {
const pathExists = existingRulePaths.has(togglePath)
if (!pathExists) {
delete updatedToggles[togglePath]
}
}
} else {
// FILE CASE
// Add toggle for this file
const pathHasToggle = rulesDirectoryPath in updatedToggles
if (!pathHasToggle) {
updatedToggles[rulesDirectoryPath] = true
}
// Remove toggles for any other paths
for (const togglePath in updatedToggles) {
if (togglePath !== rulesDirectoryPath) {
delete updatedToggles[togglePath]
}
}
}
} else {
// PATH DOESN'T EXIST CASE
// Clear all toggles since the path doesn't exist
for (const togglePath in updatedToggles) {
delete updatedToggles[togglePath]
}
}
} catch (error) {
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
}
return updatedToggles
}
export async function refreshClineRulesToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
): Promise<{
globalToggles: ClineRulesToggles
localToggles: ClineRulesToggles
}> {
// Global toggles
const globalClineRulesToggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
await updateGlobalState(context, "globalClineRulesToggles", updatedGlobalToggles)
// Local toggles
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
return {
globalToggles: updatedGlobalToggles,
localToggles: updatedLocalToggles,
}
}
@@ -0,0 +1,47 @@
import { BrowserConnectionInfo } from "../../../shared/proto/browser"
import { EmptyRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
/**
* Get information about the current browser connection
* @param controller The controller instance
* @param request The request message
* @returns The browser connection info
*/
export async function getBrowserConnectionInfo(controller: Controller, request: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const { browserSettings } = await getAllExtensionState(controller.context)
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
if (controller.task?.browserSession) {
// Access the browser session through the controller's task property
// Using indexer notation to access private property
const browserSession = controller.task.browserSession
const connectionInfo = browserSession.getConnectionInfo()
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
return {
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host || "", // Ensure host is never undefined
}
}
// Fallback to browser settings if no active browser session
return {
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost || "",
}
} catch (error: unknown) {
console.error("Error getting browser connection info:", error)
return {
isConnected: false,
isRemote: false,
host: "",
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create browser service registry
const browserService = createServiceRegistry("browser")
// Export the method handler type and registration function
export type BrowserMethodHandler = ServiceMethodHandler
export const registerMethod = browserService.registerMethod
// Export the request handler
export const handleBrowserServiceRequest = browserService.handleRequest
// Register all browser methods
registerAllMethods()
+14
View File
@@ -0,0 +1,14 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
import { testBrowserConnection } from "./testBrowserConnection"
// Register all browser service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
registerMethod("testBrowserConnection", testBrowserConnection)
}
@@ -0,0 +1,63 @@
import { BrowserConnection } from "../../../shared/proto/browser"
import { StringRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
import { BrowserSession } from "../../../services/browser/BrowserSession"
import { discoverChromeInstances } from "../../../services/browser/BrowserDiscovery"
/**
* Test connection to a browser instance
* @param controller The controller instance
* @param request The request message
* @returns The browser connection result
*/
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
try {
const { browserSettings } = await getAllExtensionState(controller.context)
const browserSession = new BrowserSession(controller.context, browserSettings)
const text = request.value || ""
// If no text is provided, try auto-discovery
if (!text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
return {
success: result.success,
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint || "",
}
} else {
return {
success: false,
message:
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
}
}
} catch (error) {
return {
success: false,
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(text)
return {
success: result.success,
message: result.message,
endpoint: result.endpoint || "",
}
}
} catch (error) {
return {
success: false,
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
}
}
+81
View File
@@ -0,0 +1,81 @@
import { Controller } from "./index"
import { handleBrowserServiceRequest } from "./browser/index"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
/**
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor(private controller: Controller) {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @returns The response message or error
*/
async handleRequest(
service: string,
method: string,
message: any,
requestId: string,
): Promise<{
message?: any
error?: string
request_id: string
}> {
try {
// Handle BrowserService requests
if (service === "cline.BrowserService") {
return {
message: await handleBrowserServiceRequest(this.controller, method, message),
request_id: requestId,
}
}
throw new Error(`Unknown service: ${service}`)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
}
/**
* Handle a gRPC request from the webview
* @param controller The controller instance
* @param request The gRPC request
*/
export async function handleGrpcRequest(
controller: Controller,
request: {
service: string
method: string
message: any
request_id: string
},
) {
try {
const grpcHandler = new GrpcHandler(controller)
const response = await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id)
// Send the response back to the webview
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: response,
})
} catch (error) {
// Send error response
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: request.request_id,
},
})
}
}
+65
View File
@@ -0,0 +1,65 @@
import { Controller } from "./index"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (controller: Controller, message: any) => Promise<any>
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
/**
* Create a new service registry
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
this.serviceName = serviceName
}
/**
* Register a method handler
* @param methodName The name of the method to register
* @param handler The handler function for the method
*/
registerMethod(methodName: string, handler: ServiceMethodHandler): void {
this.methodRegistry[methodName] = handler
console.log(`Registered ${this.serviceName} method: ${methodName}`)
}
/**
* Handle a service request
* @param controller The controller instance
* @param method The method name
* @param message The request message
* @returns The response message
*/
async handleRequest(controller: Controller, method: string, message: any): Promise<any> {
const handler = this.methodRegistry[method]
if (!handler) {
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
}
return handler(controller, message)
}
}
/**
* Create a service registry factory function
* @param serviceName The name of the service
* @returns An object with register and handle functions
*/
export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler) => registry.registerMethod(methodName, handler),
handleRequest: (controller: Controller, method: string, message: any) =>
registry.handleRequest(controller, method, message),
}
}
+61 -148
View File
@@ -2,13 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import crypto from "crypto"
import { execa } from "execa"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest } from "./grpc-handler"
import { buildApiHandler } from "../../api"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "../../integrations/misc/export-markdown"
@@ -36,17 +35,21 @@ import { searchCommits } from "../../utils/git"
import { getWorkspacePath } from "../../utils/path"
import { getTotalTasksSize } from "../../utils/storage"
import { openMention } from "../mentions"
import { GlobalFileNames } from "../storage/disk"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "../../shared/cline-rules"
import { refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -58,7 +61,7 @@ export class Controller {
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
private task?: Task
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -74,8 +77,8 @@ export class Controller {
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.mcpHub = new McpHub(
() => this.ensureMcpServersDirectoryExists(),
() => this.ensureSettingsDirectoryExists(),
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
@@ -256,6 +259,13 @@ export class Controller {
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "showChatView": {
this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
break
}
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
@@ -301,88 +311,6 @@ export class Controller {
await this.postStateToWebview()
}
break
case "getBrowserConnectionInfo":
try {
// Get the current browser session from Cline if it exists
if (this.task?.browserSession) {
const connectionInfo = this.task.browserSession.getConnectionInfo()
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host,
})
} else {
// If no active browser session, just return the settings
const { browserSettings } = await getAllExtensionState(this.context)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
})
}
} catch (error) {
console.error("Error getting browser connection info:", error)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: false,
})
}
break
case "testBrowserConnection":
try {
const { browserSettings } = await getAllExtensionState(this.context)
const browserSession = new BrowserSession(this.context, browserSettings)
// If no text is provided, try auto-discovery
if (!message.text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint,
})
} else {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
})
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(message.text)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: result.message,
endpoint: result.endpoint,
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
})
}
break
case "discoverBrowser":
try {
const discoveredHost = await discoverChromeInstances()
@@ -501,6 +429,10 @@ export class Controller {
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
break
case "openImage":
openImage(message.text!)
break
@@ -528,7 +460,7 @@ export class Controller {
break
}
case "checkpointRestore": {
await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superceded by a new message eg add deleted_api_reqs
await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superseded by a new message eg add deleted_api_reqs
// cancel task waits for any open editor to be reverted and starts a new cline instance
if (message.number) {
// wait for messages to be loaded
@@ -538,7 +470,7 @@ export class Controller {
console.error("Failed to init new cline instance")
})
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
await this.task?.restoreCheckpoint(message.number, message.text! as ClineCheckpointRestore)
await this.task?.restoreCheckpoint(message.number, message.text! as ClineCheckpointRestore, message.offset)
}
break
}
@@ -678,6 +610,30 @@ export class Controller {
}
break
}
case "toggleClineRule": {
const { isGlobal, rulePath, enabled } = message
if (rulePath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
if (isGlobal) {
const toggles =
((await getGlobalState(this.context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
toggles[rulePath] = enabled
await updateGlobalState(this.context, "globalClineRulesToggles", toggles)
} else {
const toggles =
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
toggles[rulePath] = enabled
await updateWorkspaceState(this.context, "localClineRulesToggles", toggles)
}
await this.postStateToWebview()
} else {
console.error("toggleClineRule: Missing or invalid parameters", {
rulePath,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
@@ -912,6 +868,12 @@ export class Controller {
}
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -1106,61 +1068,6 @@ export class Controller {
}
}
// MCP
async getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
async ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await this.getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
// VSCode LM API
private async getVsCodeLmModels() {
@@ -1813,7 +1720,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
}
// if we tried to get a task that doesn't exist, remove it from state
// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
// FIXME: this seems to happen sometimes when the json file doesn't save to disk for some reason
await this.deleteTaskFromState(id)
throw new Error("Task not found")
}
@@ -1945,8 +1852,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
globalClineRulesToggles,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@@ -1969,6 +1880,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
telemetrySetting,
planActSeparateModelsSetting,
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
}
}
@@ -1990,7 +1903,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// conversation history to send in API requests
/*
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way thats creating cyclic references, or the API returns a function or a Symbol as part of the message content.
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way that's creating cyclic references, or the API returns a function or a Symbol as part of the message content.
VSCode docs about state: "The value must be JSON-stringifyable ... value — A value. MUST not contain cyclic references."
For now we'll store the conversation history in memory, and if we need to store in state directly we'd need to do a manual conversion to ensure proper json stringification.
*/
+6 -2
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as diff from "diff"
import * as path from "path"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
import { McpToolCallResponse } from "../../shared/mcp"
export const formatResponse = {
duplicateFileReadNotice: () =>
@@ -204,10 +205,13 @@ Otherwise, if you have not completed the task and do not need additional informa
clineIgnoreInstructions: (content: string) =>
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
clineRulesDirectoryInstructions: (cwd: string, content: string) =>
clineRulesGlobalDirectoryInstructions: (globalClineRulesFilePath: string, content: string) =>
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory, located at ${globalClineRulesFilePath.toPosix()}, where the user has specified instructions for all working directories:\n\n${content}`,
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
clineRulesFileInstructions: (cwd: string, content: string) =>
clineRulesLocalFileInstructions: (cwd: string, content: string) =>
`# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
}
+7 -3
View File
@@ -619,7 +619,8 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
export function addUserInstructions(
settingsCustomInstructions?: string,
clineRulesFileInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
preferredLanguageInstructions?: string,
) {
@@ -630,8 +631,11 @@ export function addUserInstructions(
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (clineRulesFileInstructions) {
customInstructions += clineRulesFileInstructions + "\n\n"
if (globalClineRulesFileInstructions) {
customInstructions += globalClineRulesFileInstructions + "\n\n"
}
if (localClineRulesFileInstructions) {
customInstructions += localClineRulesFileInstructions + "\n\n"
}
if (clineIgnoreInstructions) {
customInstructions += clineIgnoreInstructions
+67
View File
@@ -5,6 +5,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineMessage } from "../../shared/ExtensionMessage"
import { TaskMetadata } from "../context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
contextHistory: "context_history.json",
@@ -15,6 +18,42 @@ export const GlobalFileNames = {
taskMetadata: "task_metadata.json",
}
export async function getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
const globalStoragePath = context.globalStorageUri.fsPath
const taskDir = path.join(globalStoragePath, "tasks", taskId)
@@ -22,6 +61,34 @@ export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext
return taskDir
}
export async function ensureRulesDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const clineRulesDir = path.join(userDocumentsPath, "Cline", "Rules")
try {
await fs.mkdir(clineRulesDir, { recursive: true })
} catch (error) {
return path.join(os.homedir(), "Documents", "Cline", "Rules") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine because we will fail gracefully with a path that does not exist
}
return clineRulesDir
}
export async function ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
export async function ensureSettingsDirectoryExists(context: vscode.ExtensionContext): Promise<string> {
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
export async function getSavedApiConversationHistory(
context: vscode.ExtensionContext,
taskId: string,
+4
View File
@@ -19,6 +19,7 @@ export type SecretKey =
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
@@ -47,6 +48,7 @@ export type GlobalStateKey =
| "openRouterModelInfo"
| "openRouterProviderSorting"
| "autoApprovalSettings"
| "globalClineRulesToggles"
| "browserSettings"
| "chatSettings"
| "vsCodeLmModelSelector"
@@ -71,3 +73,5 @@ export type GlobalStateKey =
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "favoritedModelIds"
export type LocalStateKey = "localClineRulesToggles"
+7
View File
@@ -10,6 +10,7 @@ import { BrowserSettings } from "../../shared/BrowserSettings"
import { ChatSettings } from "../../shared/ChatSettings"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { UserInfo } from "../../shared/UserInfo"
import { ClineRulesToggles } from "../../shared/cline-rules"
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
@@ -121,6 +122,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sambanovaApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -191,6 +193,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
])
let apiProvider: ApiProvider
@@ -207,6 +210,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
}
}
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
@@ -291,6 +296,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
+121 -27
View File
@@ -70,21 +70,27 @@ import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "../context/context-management/context-error-handling"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ContextManager } from "../context/context-management/ContextManager"
import { getClineRules } from "../context/instructions/user-instructions/cline-rules"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import {
ensureRulesDirectoryExists,
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
saveClineMessages,
} from "../storage/disk"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "../context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "../storage/state"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.ContentBlockParam>
@@ -301,9 +307,13 @@ export class Task {
}
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore) {
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs)
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
// Find the last message before messageIndex that has a lastCheckpointHash
const lastHashIndex = findLastIndex(this.clineMessages.slice(0, messageIndex), (m) => m.lastCheckpointHash !== undefined)
const message = this.clineMessages[messageIndex]
const lastMessageWithHash = this.clineMessages[lastHashIndex]
if (!message) {
console.error("Message not found", this.clineMessages)
return
@@ -336,6 +346,14 @@ export class Task {
vscode.window.showErrorMessage("Failed to restore checkpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
} else if (offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
try {
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
vscode.window.showErrorMessage("Failed to restore offsetcheckpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
}
break
}
@@ -433,7 +451,7 @@ export class Task {
return
}
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we cant show diff outside of workspace?
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we can't show diff outside of workspace?
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
@@ -878,7 +896,7 @@ export class Task {
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldn't be initialized when opening a old task, and it was because we were waiting for resume)
// This is important in case the user deletes messages without resuming the task first
this.apiConversationHistory = await getSavedApiConversationHistory(this.getContext(), this.taskId)
@@ -1119,34 +1137,85 @@ export class Task {
let userFeedback: { text?: string; images?: string[] } | undefined
let didContinue = false
const sendCommandOutput = async (line: string): Promise<void> => {
// Chunked terminal output buffering
const CHUNK_LINE_COUNT = 20
const CHUNK_BYTE_SIZE = 2048 // 2KB
const CHUNK_DEBOUNCE_MS = 100
let outputBuffer: string[] = []
let outputBufferSize: number = 0
let chunkTimer: NodeJS.Timeout | null = null
let chunkEnroute = false
const flushBuffer = async (force = false) => {
if (chunkEnroute || outputBuffer.length === 0) {
if (force && !chunkEnroute && outputBuffer.length > 0) {
// If force is true and no chunkEnroute, flush anyway
} else {
return
}
}
const chunk = outputBuffer.join("\n")
outputBuffer = []
outputBufferSize = 0
chunkEnroute = true
try {
const { response, text, images } = await this.ask("command_output", line)
const { response, text, images } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// proceed while running
} else {
userFeedback = { text, images }
}
didContinue = true
process.continue() // continue past the await
process.continue()
} catch {
// This can only happen if this ask promise was ignored, so ignore this error
// ask promise was ignored
} finally {
chunkEnroute = false
// If more output accumulated while chunkEnroute, flush again
if (outputBuffer.length > 0) {
await flushBuffer()
}
}
}
const scheduleFlush = () => {
if (chunkTimer) {
clearTimeout(chunkTimer)
}
chunkTimer = setTimeout(() => flushBuffer(), CHUNK_DEBOUNCE_MS)
}
let result = ""
process.on("line", (line) => {
result += line + "\n"
if (!didContinue) {
sendCommandOutput(line)
outputBuffer.push(line)
outputBufferSize += Buffer.byteLength(line, "utf8")
// Flush if buffer is large enough
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
flushBuffer()
} else {
scheduleFlush()
}
} else {
this.say("command_output", line)
}
})
let completed = false
process.once("completed", () => {
process.once("completed", async () => {
completed = true
// Flush any remaining buffered output
if (!didContinue && outputBuffer.length > 0) {
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
await flushBuffer(true)
}
})
process.once("no_shell_integration", async () => {
@@ -1279,7 +1348,12 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const clineRulesFileInstructions = await getClineRules(cwd)
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1289,14 +1363,16 @@ export class Task {
if (
settingsCustomInstructions ||
clineRulesFileInstructions ||
globalClineRulesFileInstructions ||
localClineRulesFileInstructions ||
clineIgnoreInstructions ||
preferredLanguageInstructions
) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addUserInstructions(
settingsCustomInstructions,
clineRulesFileInstructions,
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
clineIgnoreInstructions,
preferredLanguageInstructions,
)
@@ -2412,7 +2488,7 @@ export class Task {
try {
if (block.partial) {
if (this.shouldAutoApproveTool(block.name)) {
// since depending on an upcoming parameter, requiresApproval this may become an ask - we cant partially stream a say prematurely. So in this particular case we have to wait for the requiresApproval parameter to be completed before presenting it.
// since depending on an upcoming parameter, requiresApproval this may become an ask - we can't partially stream a say prematurely. So in this particular case we have to wait for the requiresApproval parameter to be completed before presenting it.
// await this.say(
// "command",
// removeClosingTag("command", command),
@@ -2458,7 +2534,7 @@ export class Task {
let didAutoApprove = false
// If the model says this command is safe and auto aproval for safe commands is true, execute the command
// If the model says this command is safe and auto approval for safe commands is true, execute the command
// If the model says the command is risky, but *BOTH* auto approve settings are true, execute the command
const autoApproveResult = this.shouldAutoApproveTool(block.name)
const [autoApproveSafe, autoApproveAll] = Array.isArray(autoApproveResult)
@@ -2614,8 +2690,13 @@ export class Task {
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
// TODO: add progress indicator and ability to parse images and non-text responses
const toolResultPretty =
// TODO: add progress indicator
const toolResultImages =
toolResult?.content
.filter((item) => item.type === "image")
.map((item) => `data:${item.mimeType};base64,${item.data}`) || []
let toolResultText =
(toolResult?.isError ? "Error:\n" : "") +
toolResult?.content
.map((item) => {
@@ -2630,8 +2711,21 @@ export class Task {
})
.filter(Boolean)
.join("\n\n") || "(No response)"
await this.say("mcp_server_response", toolResultPretty)
pushToolResult(formatResponse.toolResult(toolResultPretty))
// webview extracts images from the text response to display in the UI
const toolResultToDisplay =
toolResultText + toolResultImages?.map((image) => `\n\n${image}`).join("")
await this.say("mcp_server_response", toolResultToDisplay)
// MCP's might return images to display to the user, but the model may not support them
const supportsImages = this.api.getModel().info.supportsImages ?? false
if (toolResultImages.length > 0 && !supportsImages) {
toolResultText += `\n\n[${toolResultImages.length} images were provided in the response, and while they are displayed to the user, you do not have the ability to view them.]`
}
// only passes in images if model supports them
pushToolResult(
formatResponse.toolResult(toolResultText, supportsImages ? toolResultImages : undefined),
)
await this.saveCheckpoint()
@@ -2982,7 +3076,7 @@ export class Task {
)
} else {
// last message is completion_result
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
// we have command string, which means we have the result as well, so finish it (doesn't have to exist yet)
await this.say("completion_result", removeClosingTag("result", result), undefined, false)
await this.saveCheckpoint(true)
await addNewChangesFlagToLastCompletionResultMessage()
@@ -3018,7 +3112,7 @@ export class Task {
let commandResult: ToolResponse | undefined
if (command) {
if (lastMessage && lastMessage.ask !== "command") {
// havent sent a command message yet so first send completion_result then command
// haven't sent a command message yet so first send completion_result then command
await this.say("completion_result", result, undefined, false)
await this.saveCheckpoint(true)
await addNewChangesFlagToLastCompletionResultMessage()
@@ -3387,7 +3481,7 @@ export class Task {
if (this.didRejectTool) {
// userContent has a tool rejection, so interrupt the assistant's response to present the user's feedback
assistantMessage += "\n\n[Response interrupted by user feedback]"
// this.userMessageContentReady = true // instead of setting this premptively, we allow the present iterator to finish and set userMessageContentReady when its ready
// this.userMessageContentReady = true // instead of setting this preemptively, we allow the present iterator to finish and set userMessageContentReady when its ready
break
}
@@ -3442,7 +3536,7 @@ export class Task {
partialBlocks.forEach((block) => {
block.partial = false
})
// this.assistantMessageContent.forEach((e) => (e.partial = false)) // cant just do this bc a tool could be in the middle of executing ()
// this.assistantMessageContent.forEach((e) => (e.partial = false)) // can't just do this bc a tool could be in the middle of executing ()
if (partialBlocks.length > 0) {
this.presentAssistantMessage() // if there is content to update then it will complete and update this.userMessageContentReady to true, which we pwaitfor before making the next request. all this is really doing is presenting the last partial message that we just set to complete
}
+1 -1
View File
@@ -193,7 +193,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicitly allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
+18
View File
@@ -397,6 +397,24 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
// Register the focusChatInput command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.focusChatInput", () => {
let visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
visibleWebview = WebviewProvider.getSidebarInstance()
// showing the extension will call didBecomeVisible which focuses it already
// but it doesn't focus if a tab is selected which focusChatInput accounts for
}
visibleWebview?.controller.postMessageToWebview({
type: "action",
action: "focusChatInput",
})
}),
)
// Set up test server if in test mode
if (IS_TEST === "true") {
createTestServer(sidebarWebview)
@@ -61,7 +61,7 @@ class DiagnosticsMonitor {
return currentDiagnostics
}
let timeout = 300 // only way this happens is if theres no errors
let timeout = 300 // only way this happens is if there's no errors
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
+1 -1
View File
@@ -289,7 +289,7 @@ export class DiffViewProvider {
updatedDocument.positionAt(updatedDocument.getText().length),
)
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
console.log(`File ${absolutePath} has been reverted to its original content.`)
+2 -2
View File
@@ -95,7 +95,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
}
// first few chunks could be the command being echoed back, so we must ignore
// note this means that 'echo' commands wont work
// note this means that 'echo' commands won't work
if (!didOutputNonCommand) {
const lines = data.split("\n")
for (let i = 0; i < lines.length; i++) {
@@ -145,7 +145,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
)
// For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
// For non-immediately returning commands we want to show loading spinner right away but this wouldn't happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
if (!didEmitEmptyLine && !this.fullOutput && data) {
this.emit("line", "") // empty line to indicate start of command output stream
didEmitEmptyLine = true
+1 -1
View File
@@ -120,7 +120,7 @@ export async function searchWorkspaceFiles(
limit: limit * 2,
})
// The min threshold value will require some testing and tuning as the scores are exponential, and exagerated
// The min threshold value will require some testing and tuning as the scores are exponential, and exaggerated
const MIN_SCORE_THRESHOLD = 100
// Filter results by score and map to original items
+245
View File
@@ -0,0 +1,245 @@
import * as path from "path"
import * as vscode from "vscode"
import { execa } from "execa"
import { Logger } from "../../services/logging/Logger"
import { WebviewProvider } from "../../core/webview"
/**
* Gets a valid workspace path for Git operations
* @param visibleWebview The visible webview instance
* @returns A valid workspace path
*/
export function getWorkspacePath(visibleWebview: WebviewProvider): string {
// First try to get the path from the controller's state
let workspacePath = visibleWebview.controller.context.workspaceState.get<string>("cwd") || ""
// If workspace path is empty, try to get it from the active workspace folder
if (!workspacePath) {
const workspaceFolders = vscode.workspace.workspaceFolders
if (workspaceFolders && workspaceFolders.length > 0) {
workspacePath = workspaceFolders[0].uri.fsPath
Logger.log(`Using workspace folder path: ${workspacePath}`)
} else {
// If no workspace folder is open, use the extension directory as a fallback
workspacePath = path.join(__dirname, "..", "..", "..")
Logger.log(`No workspace folder found, using extension directory: ${workspacePath}`)
}
}
return workspacePath
}
/**
* Validates that the workspace path is valid and writable for Git operations
* @param workspacePath The workspace path to validate
* @throws Error if the workspace path is invalid or not writable
*/
export async function validateWorkspacePath(workspacePath: string): Promise<void> {
// Check if workspace path is valid
if (!workspacePath || workspacePath === "/") {
throw new Error(`Invalid workspace path: ${workspacePath}. Cannot initialize Git repository.`)
}
// Check if the directory exists
try {
const { stdout } = await execa("test", ["-d", workspacePath])
} catch (error) {
throw new Error(`Workspace path does not exist or is not a directory: ${workspacePath}`)
}
// Check if the directory is writable
try {
const testFile = path.join(workspacePath, ".cline_write_test")
await execa("touch", [testFile])
await execa("rm", [testFile])
} catch (error) {
throw new Error(`Workspace path is not writable: ${workspacePath}. Error: ${error.message}`)
}
Logger.log(`Validated workspace path: ${workspacePath}`)
}
/**
* Cleans up any existing Git repository in the specified workspace path
* @param workspacePath The workspace path to clean up
*/
export async function cleanupPreviousGit(workspacePath: string): Promise<void> {
const gitDir = path.join(workspacePath, ".git")
try {
// Check if .git directory exists using execa since we're already using it
try {
await execa("test", ["-d", gitDir])
// If we get here, the directory exists
Logger.log(`Removing existing Git repository in ${workspacePath}`)
// Use rm -rf to remove the directory
await execa("rm", ["-rf", gitDir])
Logger.log(`Removed existing Git repository`)
} catch (error) {
// Directory doesn't exist, which is fine
Logger.log(`No existing Git repository found in ${workspacePath}`)
}
} catch (error) {
Logger.log(`Warning: Failed to remove existing Git repository: ${error.message}`)
}
}
/**
* Initializes a Git repository in the specified workspace path
* @param workspacePath The workspace path to initialize Git in
* @returns True if the repository was newly initialized
*/
export async function initializeGitRepository(workspacePath: string): Promise<boolean> {
// Validate workspace path before proceeding
await validateWorkspacePath(workspacePath)
// Clean up any existing Git repository
await cleanupPreviousGit(workspacePath)
// Initialize a new Git repository
Logger.log(`Initializing Git repository in ${workspacePath}`)
try {
await execa("git", ["init"], { cwd: workspacePath })
await execa("git", ["config", "user.name", "Cline Evaluation"], { cwd: workspacePath })
await execa("git", ["config", "user.email", "cline@example.com"], { cwd: workspacePath })
// Try to create an initial commit, but don't fail if there are no files to commit
try {
// Check if there are any files to commit
const { stdout: statusOutput } = await execa("git", ["status", "--porcelain"], { cwd: workspacePath })
if (statusOutput.trim()) {
// There are files to commit
await execa("git", ["add", "."], { cwd: workspacePath })
await execa("git", ["commit", "-m", "Initial commit for evaluation"], { cwd: workspacePath })
Logger.log("Created initial Git commit in " + workspacePath)
} else {
// No files to commit, create an empty commit
Logger.log("No files to commit, creating empty initial commit")
try {
// Create an empty commit with --allow-empty
await execa("git", ["commit", "--allow-empty", "-m", "Initial empty commit for evaluation"], {
cwd: workspacePath,
})
Logger.log("Created empty initial commit in " + workspacePath)
} catch (emptyCommitError) {
// Even empty commit failed, but we'll continue anyway
Logger.log(`Warning: Failed to create empty commit: ${emptyCommitError.message}`)
}
}
} catch (commitError) {
// Initial commit failed, but Git is still initialized
Logger.log(`Warning: Failed to create initial commit: ${commitError.message}`)
Logger.log("Continuing without initial commit")
}
return true
} catch (gitError) {
// Only throw if Git initialization itself failed
const errorMessage = `Failed to initialize Git repository: ${gitError.message}`
Logger.log(errorMessage)
throw new Error(errorMessage)
}
}
/**
* Gets the file changes between the current state and the initial state
* @param workspacePath The workspace path to check for changes
* @returns Object containing lists of created, modified, and deleted files, plus the full diff
*/
export async function getFileChanges(workspacePath: string): Promise<{
created: string[]
modified: string[]
deleted: string[]
diff: string
}> {
// Validate workspace path before proceeding
await validateWorkspacePath(workspacePath)
// Make sure all changes are staged so they appear in the diff
Logger.log(`Staging all changes in ${workspacePath} for diff`)
try {
// First check if there are any untracked files
const { stdout: untrackedOutput } = await execa("git", ["ls-files", "--others", "--exclude-standard"], {
cwd: workspacePath,
})
if (untrackedOutput.trim()) {
Logger.log(`Found untracked files: ${untrackedOutput}`)
}
// Stage all changes including untracked files
await execa("git", ["add", "-A"], { cwd: workspacePath })
Logger.log("Staged all changes for diff")
} catch (error) {
Logger.log(`Warning: Failed to stage changes: ${error.message}`)
}
try {
// Get list of changed files
const { stdout: statusOutput } = await execa("git", ["status", "--porcelain"], { cwd: workspacePath })
Logger.log(`Git status output: ${statusOutput || "(empty)"}`)
const created: string[] = []
const modified: string[] = []
const deleted: string[] = []
// Parse git status output
statusOutput
.split("\n")
.filter(Boolean)
.forEach((line) => {
const status = line.substring(0, 2).trim()
const file = line.substring(3)
if (status === "A" || status === "??") {
created.push(file)
} else if (status === "M") {
modified.push(file)
} else if (status === "D") {
deleted.push(file)
}
})
// Get the full diff - include both staged and unstaged changes
const { stdout: diffOutput } = await execa("git", ["diff", "--staged"], { cwd: workspacePath })
Logger.log(`Git diff output length: ${diffOutput.length} characters`)
// If there's no diff, try getting the diff of unstaged changes
let finalDiff = diffOutput
if (!finalDiff) {
const { stdout: unstaged } = await execa("git", ["diff"], { cwd: workspacePath })
finalDiff = unstaged
Logger.log(`Unstaged git diff output length: ${unstaged.length} characters`)
}
return {
created,
modified,
deleted,
diff: finalDiff,
}
} catch (error) {
// Throw the error instead of returning a fallback
const errorMessage = `Error getting file changes: ${error.message}`
Logger.log(errorMessage)
throw new Error(errorMessage)
}
}
/**
* Calculates the tool success rate based on calls and failures
* @param toolCalls Record of tool calls by name
* @param toolFailures Record of tool failures by name
* @returns The success rate as a number between 0 and 1
*/
export function calculateToolSuccessRate(toolCalls: Record<string, number>, toolFailures: Record<string, number>): number {
const totalCalls = Object.values(toolCalls).reduce((a, b) => a + b, 0)
const totalFailures = Object.values(toolFailures).reduce((a, b) => a + b, 0)
if (totalCalls === 0) {
return 1.0 // No calls means no failures
}
return 1.0 - totalFailures / totalCalls
}
+282 -4
View File
@@ -1,11 +1,66 @@
import * as http from "http"
import * as vscode from "vscode"
import * as path from "path"
import { execa } from "execa"
import { Logger } from "../../services/logging/Logger"
import { WebviewProvider } from "../../core/webview"
import { AutoApprovalSettings } from "../../shared/AutoApprovalSettings"
import { updateGlobalState, getAllExtensionState } from "../../core/storage/state"
import {
getWorkspacePath,
validateWorkspacePath,
initializeGitRepository,
getFileChanges,
calculateToolSuccessRate,
} from "./GitHelper"
import { updateGlobalState, getAllExtensionState, updateApiConfiguration, storeSecret } from "../../core/storage/state"
import { ClineAsk, ExtensionMessage } from "../../shared/ExtensionMessage"
import { ApiProvider } from "../../shared/api"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import { getSavedClineMessages, getSavedApiConversationHistory } from "../../core/storage/disk"
/**
* Creates a tracker to monitor tool calls and failures during task execution
* @param webviewProvider The webview provider instance
* @returns Object tracking tool calls and failures
*/
function createToolCallTracker(webviewProvider: WebviewProvider): {
toolCalls: Record<string, number>
toolFailures: Record<string, number>
} {
const tracker = {
toolCalls: {} as Record<string, number>,
toolFailures: {} as Record<string, number>,
}
// Intercept messages to track tool usage
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Track tool calls
if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
const toolName = (message.partialMessage.text as any)?.tool
if (toolName) {
tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
}
}
// Track tool failures
if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
const errorText = message.partialMessage.text
if (errorText && errorText.includes("Error executing tool")) {
const match = errorText.match(/Error executing tool: (\w+)/)
if (match && match[1]) {
const toolName = match[1]
tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
}
}
}
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
return tracker
}
// Task completion tracking
let taskCompletionResolver: (() => void) | null = null
@@ -74,6 +129,13 @@ async function updateAutoApprovalSettings(context: vscode.ExtensionContext, prov
* @returns The created HTTP server instance
*/
export function createTestServer(webviewProvider?: WebviewProvider): http.Server {
// Try to show the Cline sidebar
Logger.log("[createTestServer] Opening Cline in sidebar...")
vscode.commands.executeCommand("workbench.view.claude-dev-ActivityBar")
// Then ensure the webview is focused/loaded
vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
// Update auto approval settings if webviewProvider is available
if (webviewProvider?.controller?.context) {
updateAutoApprovalSettings(webviewProvider.controller.context, webviewProvider)
@@ -93,6 +155,19 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
return
}
// Handle shutdown request
if (req.method === "POST" && req.url === "/shutdown") {
res.writeHead(200)
res.end(JSON.stringify({ success: true, message: "Server shutting down" }))
// Shut down the server after sending the response
setTimeout(() => {
shutdownTestServer()
}, 100)
return
}
// Only handle POST requests to /task
if (req.method !== "POST" || req.url !== "/task") {
res.writeHead(404)
@@ -109,7 +184,7 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
req.on("end", async () => {
try {
// Parse the JSON body
const { task } = JSON.parse(body)
const { task, apiKey } = JSON.parse(body)
if (!task) {
res.writeHead(400)
@@ -129,9 +204,76 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
Logger.log(`Test server initiating task: ${task}`)
try {
// Get and validate the workspace path
const workspacePath = getWorkspacePath(visibleWebview)
Logger.log(`Using workspace path: ${workspacePath}`)
// Validate workspace path before proceeding with any operations
try {
await validateWorkspacePath(workspacePath)
} catch (error) {
Logger.log(`Workspace validation failed: ${error.message}`)
res.writeHead(500)
res.end(
JSON.stringify({
error: `Workspace validation failed: ${error.message}. Please open a workspace folder in VSCode before running the test.`,
workspacePath,
}),
)
return
}
// Initialize Git repository before starting the task
try {
const wasNewlyInitialized = await initializeGitRepository(workspacePath)
if (wasNewlyInitialized) {
Logger.log(`Initialized new Git repository in ${workspacePath} before task start`)
} else {
Logger.log(`Using existing Git repository in ${workspacePath} before task start`)
}
// Log directory contents before task start
try {
const { stdout: lsOutput } = await execa("ls", ["-la", workspacePath])
Logger.log(`Directory contents before task start:\n${lsOutput}`)
} catch (lsError) {
Logger.log(`Warning: Failed to list directory contents: ${lsError.message}`)
}
} catch (gitError) {
Logger.log(`Warning: Git initialization failed: ${gitError.message}`)
Logger.log("Continuing without Git initialization")
}
// Clear any existing task
await visibleWebview.controller.clearTask()
// If API key is provided, update the API configuration
if (apiKey) {
Logger.log("API key provided, updating API configuration")
// Get current API configuration
const { apiConfiguration } = await getAllExtensionState(visibleWebview.controller.context)
// Update API configuration with API key
const updatedConfig = {
...apiConfiguration,
apiProvider: "cline" as ApiProvider,
clineApiKey: apiKey,
}
// Store the API key securely
await storeSecret(visibleWebview.controller.context, "clineApiKey", apiKey)
// Update the API configuration
await updateApiConfiguration(visibleWebview.controller.context, updatedConfig)
// Update global state to use cline provider
await updateGlobalState(visibleWebview.controller.context, "apiProvider", "cline" as ApiProvider)
// Post state to webview to reflect changes
await visibleWebview.controller.postStateToWebview()
}
// Ensure we're in Act mode before initiating the task
const { chatSettings } = await visibleWebview.controller.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
@@ -139,8 +281,47 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
await visibleWebview.controller.togglePlanActModeWithChatSettings({ mode: "act" })
}
// Initialize tool call tracker
const toolTracker = createToolCallTracker(visibleWebview)
// Record task start time
const taskStartTime = Date.now()
// Initiate the new task
const taskId = await visibleWebview.controller.initTask(task)
const result = await visibleWebview.controller.initTask(task)
// Try to get the task ID directly from the result or from the state
let taskId: string | undefined
if (typeof result === "string") {
// If initTask returns the task ID directly
taskId = result
} else {
// Wait a moment for the state to update
await new Promise((resolve) => setTimeout(resolve, 1000))
// Try to get the task ID from the controller's state
const state = await visibleWebview.controller.getStateToPostToWebview()
taskId = state.currentTaskItem?.id
// If still not found, try polling a few times
if (!taskId) {
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 500))
const updatedState = await visibleWebview.controller.getStateToPostToWebview()
taskId = updatedState.currentTaskItem?.id
if (taskId) {
break
}
}
}
}
if (!taskId) {
throw new Error("Failed to get task ID after initiating task")
}
Logger.log(`Task initiated with ID: ${taskId}`)
// Create a completion tracker for this task
const completionPromise = createTaskCompletionTracker()
@@ -154,13 +335,110 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
// Wait for either completion or timeout
await Promise.race([completionPromise, timeoutPromise])
// Return success response with the task ID
// Get task history and metrics
const taskHistory = await visibleWebview.controller.getStateToPostToWebview()
const taskData = taskHistory.taskHistory?.find((t: HistoryItem) => t.id === taskId)
// Get messages and API conversation history
let messages: any[] = []
let apiConversationHistory: any[] = []
try {
if (typeof taskId === "string") {
messages = await getSavedClineMessages(visibleWebview.controller.context, taskId)
}
} catch (error) {
Logger.log(`Error getting saved Cline messages: ${error}`)
}
try {
if (typeof taskId === "string") {
apiConversationHistory = await getSavedApiConversationHistory(
visibleWebview.controller.context,
taskId,
)
}
} catch (error) {
Logger.log(`Error getting saved API conversation history: ${error}`)
}
// Get file changes
let fileChanges
try {
// Get the workspace path using our helper function
const workspacePath = getWorkspacePath(visibleWebview)
Logger.log(`Getting file changes from workspace path: ${workspacePath}`)
// Log directory contents for debugging
try {
const { stdout: lsOutput } = await execa("ls", ["-la", workspacePath])
Logger.log(`Directory contents after task completion:\n${lsOutput}`)
} catch (lsError) {
Logger.log(`Warning: Failed to list directory contents: ${lsError.message}`)
}
// Get file changes using Git
fileChanges = await getFileChanges(workspacePath)
// If no changes were detected, use a fallback method
if (!fileChanges.created.length && !fileChanges.modified.length && !fileChanges.deleted.length) {
Logger.log("No changes detected by Git, using fallback directory scan")
// Try to get a list of all files in the directory
try {
const { stdout: findOutput } = await execa("find", [
workspacePath,
"-type",
"f",
"-not",
"-path",
"*/.*",
"-not",
"-path",
"*/node_modules/*",
])
const files = findOutput.split("\n").filter(Boolean)
// Add all files as "created" since we can't determine which ones are new
fileChanges.created = files.map((file) => path.relative(workspacePath, file))
Logger.log(`Fallback found ${fileChanges.created.length} files`)
} catch (findError) {
Logger.log(`Warning: Fallback directory scan failed: ${findError.message}`)
}
}
} catch (fileChangeError) {
Logger.log(`Error getting file changes: ${fileChangeError.message}`)
throw new Error(`Error getting file changes: ${fileChangeError.message}`)
}
// Get tool metrics
const toolMetrics = {
toolCalls: toolTracker.toolCalls,
toolFailures: toolTracker.toolFailures,
totalToolCalls: Object.values(toolTracker.toolCalls).reduce((a, b) => a + b, 0),
totalToolFailures: Object.values(toolTracker.toolFailures).reduce((a, b) => a + b, 0),
toolSuccessRate: calculateToolSuccessRate(toolTracker.toolCalls, toolTracker.toolFailures),
}
// Calculate task duration
const taskDuration = Date.now() - taskStartTime
// Return comprehensive response with all metrics and data
res.writeHead(200, { "Content-Type": "application/json" })
res.end(
JSON.stringify({
success: true,
taskId,
completed: true,
metrics: {
tokensIn: taskData?.tokensIn || 0,
tokensOut: taskData?.tokensOut || 0,
cost: taskData?.totalCost || 0,
duration: taskDuration,
...toolMetrics,
},
messages,
apiConversationHistory,
files: fileChanges,
}),
)
} catch (timeoutError) {
+1 -1
View File
@@ -157,7 +157,7 @@ async function parseFile(
formattedOutput += "|----\n"
}
// Only add the first line of the definition
// query captures includes the definition name and the definition implementation, but we only want the name (I found discrepencies in the naming structure for various languages, i.e. javascript names would be 'name' and typescript names would be 'name.definition)
// query captures includes the definition name and the definition implementation, but we only want the name (I found discrepancies in the naming structure for various languages, i.e. javascript names would be 'name' and typescript names would be 'name.definition)
if (name.includes("name") && lines[startLine]) {
formattedOutput += `${lines[startLine]}\n`
}
+10 -7
View File
@@ -9,6 +9,7 @@ import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
import { TelemetrySetting } from "./TelemetrySetting"
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
import { ClineRulesToggles } from "./cline-rules"
// webview will hold state
export interface ExtensionMessage {
@@ -43,12 +44,12 @@ export interface ExtensionMessage {
| "totalTasksSize"
| "addToInput"
| "browserConnectionResult"
| "browserConnectionInfo"
| "detectedChromePath"
| "scrollToSettings"
| "browserRelaunchResult"
| "relativePathsResponse" // Handles single and multiple path responses
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
paths?: (string | null)[] // Used for relativePathsResponse
action?:
@@ -60,6 +61,7 @@ export interface ExtensionMessage {
| "accountLoginClicked"
| "accountLogoutClicked"
| "accountButtonClicked"
| "focusChatInput"
invoke?: Invoke
state?: ExtensionState
images?: string[]
@@ -109,6 +111,11 @@ export interface ExtensionMessage {
error?: string
}
tab?: McpViewTab
grpc_response?: {
message?: any // JSON serialized protobuf message
request_id: string // Same ID as the request
error?: string // Optional error message
}
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -141,6 +148,8 @@ export interface ExtensionState {
}
version: string
vscMachineId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
}
export interface ClineMessage {
@@ -236,12 +245,6 @@ export type BrowserActionResult = {
currentMousePosition?: string
}
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
+16 -3
View File
@@ -29,17 +29,17 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
| "showChatView"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "discoverBrowser"
| "testBrowserConnection"
| "browserConnectionResult"
| "browserRelaunchResult"
| "togglePlanActMode"
| "checkpointDiff"
@@ -74,13 +74,15 @@ export interface WebviewMessage {
| "requestTotalTasksSize"
| "relaunchChromeDebugMode"
| "taskFeedback"
| "getBrowserConnectionInfo"
| "getDetectedChromePath"
| "detectedChromePath"
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
| "toggleClineRule"
// | "relaunchChromeDebugMode"
text?: string
uris?: string[] // Used for getRelativePaths
@@ -117,6 +119,17 @@ export interface WebviewMessage {
query?: string
// For toggleFavoriteModel
modelId?: string
grpc_request?: {
service: string
method: string
message: any // JSON serialized protobuf message
request_id: string // For correlating requests and responses
}
// For toggleClineRule
isGlobal?: boolean
rulePath?: string
enabled?: boolean
offset?: number
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+21
View File
@@ -606,6 +606,24 @@ export const geminiModels = {
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1"
export const openAiNativeModels = {
o3: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 10.0,
outputPrice: 40.0,
cacheReadsPrice: 2.5,
},
"o4-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
outputPrice: 4.4,
cacheReadsPrice: 0.275,
},
"gpt-4.1": {
maxTokens: 32_768,
contextWindow: 1_047_576,
@@ -613,6 +631,7 @@ export const openAiNativeModels = {
supportsPromptCache: true,
inputPrice: 2,
outputPrice: 8,
cacheReadsPrice: 0.5,
},
"gpt-4.1-mini": {
maxTokens: 32_768,
@@ -621,6 +640,7 @@ export const openAiNativeModels = {
supportsPromptCache: true,
inputPrice: 0.4,
outputPrice: 1.6,
cacheReadsPrice: 0.1,
},
"gpt-4.1-nano": {
maxTokens: 32_768,
@@ -629,6 +649,7 @@ export const openAiNativeModels = {
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
},
"o3-mini": {
maxTokens: 100_000,
+1
View File
@@ -0,0 +1 @@
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
+261
View File
@@ -0,0 +1,261 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: browser.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest, StringRequest } from "./common"
export const protobufPackage = "cline"
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string | undefined
}
export interface BrowserConnection {
success: boolean
message: string
endpoint?: string | undefined
}
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
return { isConnected: false, isRemote: false, host: undefined }
}
export const BrowserConnectionInfo: MessageFns<BrowserConnectionInfo> = {
encode(message: BrowserConnectionInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.isConnected !== false) {
writer.uint32(8).bool(message.isConnected)
}
if (message.isRemote !== false) {
writer.uint32(16).bool(message.isRemote)
}
if (message.host !== undefined) {
writer.uint32(26).string(message.host)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserConnectionInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserConnectionInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.isConnected = reader.bool()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.isRemote = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.host = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserConnectionInfo {
return {
isConnected: isSet(object.isConnected) ? globalThis.Boolean(object.isConnected) : false,
isRemote: isSet(object.isRemote) ? globalThis.Boolean(object.isRemote) : false,
host: isSet(object.host) ? globalThis.String(object.host) : undefined,
}
},
toJSON(message: BrowserConnectionInfo): unknown {
const obj: any = {}
if (message.isConnected !== false) {
obj.isConnected = message.isConnected
}
if (message.isRemote !== false) {
obj.isRemote = message.isRemote
}
if (message.host !== undefined) {
obj.host = message.host
}
return obj
},
create<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(base?: I): BrowserConnectionInfo {
return BrowserConnectionInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(object: I): BrowserConnectionInfo {
const message = createBaseBrowserConnectionInfo()
message.isConnected = object.isConnected ?? false
message.isRemote = object.isRemote ?? false
message.host = object.host ?? undefined
return message
},
}
function createBaseBrowserConnection(): BrowserConnection {
return { success: false, message: "", endpoint: undefined }
}
export const BrowserConnection: MessageFns<BrowserConnection> = {
encode(message: BrowserConnection, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.success !== false) {
writer.uint32(8).bool(message.success)
}
if (message.message !== "") {
writer.uint32(18).string(message.message)
}
if (message.endpoint !== undefined) {
writer.uint32(26).string(message.endpoint)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserConnection {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserConnection()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.success = reader.bool()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.message = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.endpoint = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserConnection {
return {
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
message: isSet(object.message) ? globalThis.String(object.message) : "",
endpoint: isSet(object.endpoint) ? globalThis.String(object.endpoint) : undefined,
}
},
toJSON(message: BrowserConnection): unknown {
const obj: any = {}
if (message.success !== false) {
obj.success = message.success
}
if (message.message !== "") {
obj.message = message.message
}
if (message.endpoint !== undefined) {
obj.endpoint = message.endpoint
}
return obj
},
create<I extends Exact<DeepPartial<BrowserConnection>, I>>(base?: I): BrowserConnection {
return BrowserConnection.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserConnection>, I>>(object: I): BrowserConnection {
const message = createBaseBrowserConnection()
message.success = object.success ?? false
message.message = object.message ?? ""
message.endpoint = object.endpoint ?? undefined
return message
},
}
export type BrowserServiceDefinition = typeof BrowserServiceDefinition
export const BrowserServiceDefinition = {
name: "BrowserService",
fullName: "cline.BrowserService",
methods: {
getBrowserConnectionInfo: {
name: "getBrowserConnectionInfo",
requestType: EmptyRequest,
requestStream: false,
responseType: BrowserConnectionInfo,
responseStream: false,
options: {},
},
testBrowserConnection: {
name: "testBrowserConnection",
requestType: StringRequest,
requestStream: false,
responseType: BrowserConnection,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+644
View File
@@ -0,0 +1,644 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: common.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
export const protobufPackage = "cline"
export interface Metadata {}
export interface EmptyRequest {
metadata?: Metadata | undefined
}
export interface Empty {}
export interface StringRequest {
metadata?: Metadata | undefined
value: string
}
export interface String {
value: string
}
export interface Int64Request {
metadata?: Metadata | undefined
value: number
}
export interface Int64 {
value: number
}
export interface BytesRequest {
metadata?: Metadata | undefined
value: Buffer
}
export interface Bytes {
value: Buffer
}
function createBaseMetadata(): Metadata {
return {}
}
export const Metadata: MessageFns<Metadata> = {
encode(_: Metadata, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Metadata {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMetadata()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Metadata {
return {}
},
toJSON(_: Metadata): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Metadata>, I>>(base?: I): Metadata {
return Metadata.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Metadata>, I>>(_: I): Metadata {
const message = createBaseMetadata()
return message
},
}
function createBaseEmptyRequest(): EmptyRequest {
return { metadata: undefined }
}
export const EmptyRequest: MessageFns<EmptyRequest> = {
encode(message: EmptyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): EmptyRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmptyRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): EmptyRequest {
return { metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined }
},
toJSON(message: EmptyRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
return obj
},
create<I extends Exact<DeepPartial<EmptyRequest>, I>>(base?: I): EmptyRequest {
return EmptyRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<EmptyRequest>, I>>(object: I): EmptyRequest {
const message = createBaseEmptyRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
return message
},
}
function createBaseEmpty(): Empty {
return {}
}
export const Empty: MessageFns<Empty> = {
encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Empty {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmpty()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Empty {
return {}
},
toJSON(_: Empty): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Empty>, I>>(base?: I): Empty {
return Empty.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Empty>, I>>(_: I): Empty {
const message = createBaseEmpty()
return message
},
}
function createBaseStringRequest(): StringRequest {
return { metadata: undefined, value: "" }
}
export const StringRequest: MessageFns<StringRequest> = {
encode(message: StringRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): StringRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseStringRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.value = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): StringRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? globalThis.String(object.value) : "",
}
},
toJSON(message: StringRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value !== "") {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<StringRequest>, I>>(base?: I): StringRequest {
return StringRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<StringRequest>, I>>(object: I): StringRequest {
const message = createBaseStringRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? ""
return message
},
}
function createBaseString(): String {
return { value: "" }
}
export const String: MessageFns<String> = {
encode(message: String, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value !== "") {
writer.uint32(10).string(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): String {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseString()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.value = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): String {
return { value: isSet(object.value) ? globalThis.String(object.value) : "" }
},
toJSON(message: String): unknown {
const obj: any = {}
if (message.value !== "") {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<String>, I>>(base?: I): String {
return String.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<String>, I>>(object: I): String {
const message = createBaseString()
message.value = object.value ?? ""
return message
},
}
function createBaseInt64Request(): Int64Request {
return { metadata: undefined, value: 0 }
}
export const Int64Request: MessageFns<Int64Request> = {
encode(message: Int64Request, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value !== 0) {
writer.uint32(16).int64(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Int64Request {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseInt64Request()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 16) {
break
}
message.value = longToNumber(reader.int64())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Int64Request {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? globalThis.Number(object.value) : 0,
}
},
toJSON(message: Int64Request): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value !== 0) {
obj.value = Math.round(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Int64Request>, I>>(base?: I): Int64Request {
return Int64Request.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Int64Request>, I>>(object: I): Int64Request {
const message = createBaseInt64Request()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? 0
return message
},
}
function createBaseInt64(): Int64 {
return { value: 0 }
}
export const Int64: MessageFns<Int64> = {
encode(message: Int64, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value !== 0) {
writer.uint32(8).int64(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Int64 {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseInt64()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.value = longToNumber(reader.int64())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Int64 {
return { value: isSet(object.value) ? globalThis.Number(object.value) : 0 }
},
toJSON(message: Int64): unknown {
const obj: any = {}
if (message.value !== 0) {
obj.value = Math.round(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Int64>, I>>(base?: I): Int64 {
return Int64.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Int64>, I>>(object: I): Int64 {
const message = createBaseInt64()
message.value = object.value ?? 0
return message
},
}
function createBaseBytesRequest(): BytesRequest {
return { metadata: undefined, value: Buffer.alloc(0) }
}
export const BytesRequest: MessageFns<BytesRequest> = {
encode(message: BytesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value.length !== 0) {
writer.uint32(18).bytes(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BytesRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBytesRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.value = Buffer.from(reader.bytes())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BytesRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? Buffer.from(bytesFromBase64(object.value)) : Buffer.alloc(0),
}
},
toJSON(message: BytesRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value.length !== 0) {
obj.value = base64FromBytes(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<BytesRequest>, I>>(base?: I): BytesRequest {
return BytesRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BytesRequest>, I>>(object: I): BytesRequest {
const message = createBaseBytesRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? Buffer.alloc(0)
return message
},
}
function createBaseBytes(): Bytes {
return { value: Buffer.alloc(0) }
}
export const Bytes: MessageFns<Bytes> = {
encode(message: Bytes, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value.length !== 0) {
writer.uint32(10).bytes(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Bytes {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBytes()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.value = Buffer.from(reader.bytes())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Bytes {
return { value: isSet(object.value) ? Buffer.from(bytesFromBase64(object.value)) : Buffer.alloc(0) }
},
toJSON(message: Bytes): unknown {
const obj: any = {}
if (message.value.length !== 0) {
obj.value = base64FromBytes(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Bytes>, I>>(base?: I): Bytes {
return Bytes.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Bytes>, I>>(object: I): Bytes {
const message = createBaseBytes()
message.value = object.value ?? Buffer.alloc(0)
return message
},
}
function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"))
}
function base64FromBytes(arr: Uint8Array): string {
return globalThis.Buffer.from(arr).toString("base64")
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString())
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER")
}
return num
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+9 -1
View File
@@ -75,6 +75,13 @@ export async function getFileSizeInKB(filePath: string): Promise<number> {
}
}
// Common OS-generated files that would appear in an otherwise clean directory
const OS_GENERATED_FILES = [
".DS_Store", // macOS Finder
"Thumbs.db", // Windows Explorer thumbnails
"desktop.ini", // Windows folder settings
]
/**
* Recursively reads a directory and returns an array of absolute file paths.
*
@@ -86,7 +93,8 @@ export const readDirectory = async (directoryPath: string) => {
try {
const filePaths = await fs
.readdir(directoryPath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
.then((entries) => entries.filter((entry) => entry.isFile()))
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
return filePaths
} catch {
+1 -1
View File
@@ -91,7 +91,7 @@ function getWindowsShellFromVSCode(): string | null {
// If the profile name indicates PowerShell, do version-based detection.
// In testing it was found these typically do not have a path, and this
// implementation manages to deductively get the corect version of PowerShell
// implementation manages to deductively get the correct version of PowerShell
if (defaultProfileName.toLowerCase().includes("powershell")) {
if (profile?.path) {
// If there's an explicit PowerShell path, return that
+23 -13
View File
@@ -27,7 +27,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -40,6 +41,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3539,6 +3541,13 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode-webview": {
"version": "1.57.5",
"resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz",
@@ -6936,18 +6945,6 @@
"uuid": "^9.0.1"
}
},
"node_modules/mermaid/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/micromark": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz",
@@ -8836,6 +8833,19 @@
}
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+3 -1
View File
@@ -33,7 +33,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -46,6 +47,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { BrowserServiceClient } from "../../services/grpc-client"
interface ConnectionInfo {
isConnected: boolean
@@ -21,29 +23,25 @@ export const BrowserSettingsMenu = () => {
})
const popoverRef = useRef<HTMLDivElement>(null)
// Get actual connection info from the browser session
// Get actual connection info from the browser session using gRPC
useEffect(() => {
// Request connection info when component mounts
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
// Listen for connection info updates
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "browserConnectionInfo") {
// Function to fetch connection info
;(async () => {
try {
console.log("[DEBUG] SENDING BROWSER CONNECTION INFO REQUEST")
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
console.log("[DEBUG] GOT BROWSER REPLY:", info, typeof info)
setConnectionInfo({
isConnected: message.isConnected,
isRemote: message.isRemote,
host: message.host,
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
})()
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
// No need for message event listeners anymore!
}, [browserSettings.remoteBrowserHost, browserSettings.remoteBrowserEnabled])
// Close popover when clicking outside
@@ -84,11 +82,22 @@ export const BrowserSettingsMenu = () => {
const toggleInfoPopover = () => {
setShowInfoPopover(!showInfoPopover)
// Request updated connection info when opening the popover
// Request updated connection info when opening the popover using gRPC
if (!showInfoPopover) {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
fetchConnectionInfo()
}
}
@@ -112,19 +121,27 @@ export const BrowserSettingsMenu = () => {
}
}
// Check connection status every second to keep icon in sync
// Check connection status every second to keep icon in sync using gRPC
useEffect(() => {
// Function to fetch connection info
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
// Request connection info immediately
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
fetchConnectionInfo()
// Set up interval to refresh every second
const intervalId = setInterval(() => {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
}, 1000)
const intervalId = setInterval(fetchConnectionInfo, 1000)
return () => clearInterval(intervalId)
}, [])
@@ -29,7 +29,7 @@ const linkContainerStyle: CSSProperties = { margin: "0" }
const linkStyle: CSSProperties = { display: "inline" }
/*
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with what's in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
*/
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
@@ -192,7 +192,12 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Reset for next page
currentStateMessages = []
nextActionMessages = []
} else if (message.say === "api_req_started" || message.say === "text" || message.say === "browser_action") {
} else if (
message.say === "api_req_started" ||
message.say === "text" ||
message.say === "reasoning" ||
message.say === "browser_action"
) {
// These messages lead to the next result, so they should always go in nextActionMessages
nextActionMessages.push(message)
} else {
@@ -511,6 +516,7 @@ const BrowserSessionRowContent = ({
switch (message.say) {
case "api_req_started":
case "text":
case "reasoning":
return (
<div style={chatRowContentContainerStyle}>
<ChatRowContent
+21 -16
View File
@@ -32,6 +32,7 @@ import SuccessButton from "@/components/common/SuccessButton"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import NewTaskPreview from "./NewTaskPreview"
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
import UserMessage from "./UserMessage"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -49,6 +50,8 @@ interface ChatRowProps {
lastModifiedMessage?: ClineMessage
isLast: boolean
onHeightChange: (isTaller: boolean) => void
inputValue?: string
sendMessageFromChatRow?: (text: string, images: string[]) => void
}
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
@@ -84,7 +87,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
const ChatRow = memo(
(props: ChatRowProps) => {
const { isLast, onHeightChange, message, lastModifiedMessage } = props
const { isLast, onHeightChange, message, lastModifiedMessage, inputValue } = props
// Store the previous height to compare with the current height
// This allows us to detect changes without causing re-renders
const prevHeightRef = useRef(0)
@@ -117,7 +120,15 @@ const ChatRow = memo(
export default ChatRow
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
export const ChatRowContent = ({
message,
isExpanded,
onToggleExpand,
lastModifiedMessage,
isLast,
inputValue,
sendMessageFromChatRow,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
@@ -864,20 +875,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
)
case "user_feedback":
return (
<div
style={{
backgroundColor: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
}}>
<span style={{ display: "block" }}>{highlightMentions(message.text)}</span>
{message.images && message.images.length > 0 && (
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
)}
</div>
<UserMessage
text={message.text}
images={message.images}
messageTs={message.ts}
sendMessageFromChatRow={sendMessageFromChatRow}
/>
)
case "user_feedback_diff":
const tool = JSON.parse(message.text || "{}") as ClineSayTool
@@ -1248,6 +1251,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
inputValue={inputValue}
/>
</div>
</>
@@ -1287,6 +1291,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
inputValue={inputValue}
/>
</div>
)
@@ -26,6 +26,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
import ContextMenu from "@/components/chat/ContextMenu"
import { ChatSettings } from "@shared/ChatSettings"
import ServersToggleModal from "./ServersToggleModal"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
interface ChatTextAreaProps {
inputValue: string
@@ -1153,7 +1154,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
display: "flex",
alignItems: "flex-center",
height: textAreaBaseHeight || 31,
bottom: 9.5, // should be 10 but doesnt look good on mac
bottom: 9.5, // should be 10 but doesn't look good on mac
zIndex: 2,
}}>
<div
@@ -1225,6 +1226,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</ButtonContainer>
</VSCodeButton>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
+11 -1
View File
@@ -461,6 +461,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
textAreaRef.current?.focus()
}
break
case "focusChatInput":
textAreaRef.current?.focus()
if (isHidden) {
// Send message back to extension to show chat view
vscode.postMessage({ type: "showChatView" })
}
break
}
break
case "selectedImages":
@@ -568,6 +575,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
"browser_action",
"browser_action_result",
"checkpoint_created",
"reasoning",
].includes(message.say!)
}
return false
@@ -789,10 +797,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === groupedMessages.length - 1}
onHeightChange={handleRowHeightChange}
inputValue={inputValue}
sendMessageFromChatRow={handleSendMessage}
/>
)
},
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange, inputValue],
)
return (
@@ -26,10 +26,12 @@ export const OptionsButtons = ({
options,
selected,
isActive,
inputValue,
}: {
options?: string[]
selected?: string
isActive?: boolean
inputValue?: string
}) => {
if (!options?.length) return null
@@ -58,7 +60,7 @@ export const OptionsButtons = ({
}
vscode.postMessage({
type: "optionsResponse",
text: option,
text: option + (inputValue ? `: ${inputValue?.trim()}` : ""),
})
}}>
{option}
@@ -74,7 +74,7 @@ const ServersToggleModal: React.FC = () => {
/>
<div className="flex justify-between items-center mb-2.5">
<div className="m-0">MCP Servers</div>
<div className="m-0 text-base font-semibold">MCP Servers</div>
<VSCodeButton
appearance="icon"
onClick={() => {
@@ -0,0 +1,185 @@
import React, { useState, useRef, forwardRef, useCallback } from "react"
import Thumbnails from "@/components/common/Thumbnails"
import { highlightMentions } from "./TaskHeader"
import { vscode } from "@/utils/vscode"
import DynamicTextArea from "react-textarea-autosize"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface UserMessageProps {
text?: string
images?: string[]
messageTs?: number // Timestamp for the message, needed for checkpoint restore
sendMessageFromChatRow?: (text: string, images: string[]) => void
}
const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, sendMessageFromChatRow }) => {
const [isEditing, setIsEditing] = useState(false)
const [editedText, setEditedText] = useState(text || "")
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const { checkpointTrackerErrorMessage } = useExtensionState()
// Create refs for the buttons to check in the blur handler
const restoreAllButtonRef = useRef<HTMLButtonElement>(null)
const restoreChatButtonRef = useRef<HTMLButtonElement>(null)
const handleClick = () => {
if (!isEditing) {
setIsEditing(true)
}
}
// Select all text when entering edit mode
React.useEffect(() => {
if (isEditing && textAreaRef.current) {
textAreaRef.current.select()
}
}, [isEditing])
const handleRestoreWorkspace = (type: string) => {
const delay = type === "task" ? 500 : 1000 // Delay for task and workspace restore
setIsEditing(false)
if (text === editedText) {
return
}
vscode.postMessage({
type: "checkpointRestore",
number: messageTs,
text: type,
offset: 1,
})
setTimeout(() => {
sendMessageFromChatRow?.(editedText, images || [])
}, delay)
}
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
// Check if focus is moving to one of our button elements
if (e.relatedTarget === restoreAllButtonRef.current || e.relatedTarget === restoreChatButtonRef.current) {
// Don't close edit mode if focus is moving to one of our buttons
return
}
// Otherwise, close edit mode
setIsEditing(false)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Escape") {
setIsEditing(false)
} else if (e.key === "Enter" && e.metaKey && !checkpointTrackerErrorMessage) {
handleRestoreWorkspace("taskAndWorkspace")
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleRestoreWorkspace("task")
}
}
return (
<div
style={{
backgroundColor: isEditing ? "unset" : "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
}}
onClick={handleClick}>
{isEditing ? (
<>
<DynamicTextArea
ref={textAreaRef}
value={editedText}
onChange={(e) => setEditedText(e.target.value)}
onBlur={(e) => handleBlur(e)}
onKeyDown={handleKeyDown}
autoFocus
style={{
width: "100%",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
borderColor: "var(--vscode-input-border)",
border: "1px solid",
borderRadius: "2px",
padding: "6px",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
boxSizing: "border-box",
resize: "none",
overflowX: "hidden",
overflowY: "scroll",
scrollbarWidth: "none",
}}
/>
<div style={{ display: "flex", gap: "8px", marginTop: "8px", justifyContent: "flex-end" }}>
{!checkpointTrackerErrorMessage && (
<RestoreButton
ref={restoreAllButtonRef}
type="taskAndWorkspace"
label="Restore All"
isPrimary={false}
onClick={handleRestoreWorkspace}
title="Restore both the chat and workspace files to this checkpoint and send your edited message"
/>
)}
<RestoreButton
ref={restoreChatButtonRef}
type="task"
label="Restore Chat"
isPrimary={true}
onClick={handleRestoreWorkspace}
title="Restore just the chat to this checkpoint and send your edited message"
/>
</div>
</>
) : (
<span style={{ display: "block" }}>{highlightMentions(editedText || text)}</span>
)}
{images && images.length > 0 && <Thumbnails images={images} style={{ marginTop: "8px" }} />}
</div>
)
}
// Reusable button component for restore actions
interface RestoreButtonProps {
type: string
label: string
isPrimary: boolean
onClick: (type: string) => void
title?: string
}
const RestoreButton = forwardRef<HTMLButtonElement, RestoreButtonProps>(({ type, label, isPrimary, onClick, title }, ref) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
onClick(type)
}
return (
<button
ref={ref}
onClick={handleClick}
title={title}
style={{
backgroundColor: isPrimary
? "var(--vscode-button-background)"
: "var(--vscode-button-secondaryBackground, var(--vscode-descriptionForeground))",
color: isPrimary
? "var(--vscode-button-foreground)"
: "var(--vscode-button-secondaryForeground, var(--vscode-foreground))",
border: "none",
padding: "4px 8px",
borderRadius: "2px",
fontSize: "9px",
cursor: "pointer",
}}>
{label}
</button>
)
})
export default UserMessage
@@ -0,0 +1,133 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import RulesToggleList from "./RulesToggleList"
const ClineRulesToggleModal: React.FC = () => {
const { globalClineRulesToggles = {}, localClineRulesToggles = {} } = useExtensionState()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
useEffect(() => {
if (isVisible) {
vscode.postMessage({ type: "refreshClineRules" })
}
}, [isVisible])
// Format global rules for display with proper typing
const globalRules = Object.entries(globalClineRulesToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
// Format local rules for display with proper typing
const localRules = Object.entries(localClineRulesToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
// Handle toggle rule
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
vscode.postMessage({
type: "toggleClineRule",
isGlobal,
rulePath,
enabled,
})
}
// Close modal when clicking outside
useClickAway(modalRef, () => {
setIsVisible(false)
})
// Calculate positions for modal and arrow
useEffect(() => {
if (isVisible && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect()
const buttonCenter = buttonRect.left + buttonRect.width / 2
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
setArrowPosition(rightPosition)
setMenuPosition(buttonRect.top + 1)
}
}, [isVisible, viewportWidth, viewportHeight])
return (
<div ref={modalRef}>
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
<VSCodeButton
appearance="icon"
aria-label="Cline Rules"
onClick={() => setIsVisible(!isVisible)}
style={{ padding: "0px 0px", height: "20px" }}>
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
<span className="codicon codicon-law flex items-center" style={{ fontSize: "12.5px", marginBottom: 1 }} />
</div>
</VSCodeButton>
</div>
{isVisible && (
<div
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
background: CODE_BLOCK_BG_COLOR,
maxHeight: "calc(100vh - 100px)",
overscrollBehavior: "contain",
}}>
<div
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
style={{
bottom: `calc(100vh - ${menuPosition}px)`,
right: arrowPosition,
background: CODE_BLOCK_BG_COLOR,
}}
/>
<div className="flex justify-between items-center mb-2.5">
<div className="m-0 text-base font-semibold">Cline Rules</div>
<VSCodeButton
appearance="icon"
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
})
setIsVisible(false)
}}>
{/* <span className="codicon codicon-gear text-[10px]"></span> */}
</VSCodeButton>
</div>
{/* Global Rules Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Rules</div>
<RulesToggleList
rules={globalRules}
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
listGap="small"
/>
</div>
{/* Local Rules Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Rules</div>
<RulesToggleList
rules={localRules}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
listGap="small"
/>
</div>
</div>
)}
</div>
)
}
export default ClineRulesToggleModal
@@ -0,0 +1,67 @@
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
const RuleRow: React.FC<{
rulePath: string
enabled: boolean
toggleRule: (rulePath: string, enabled: boolean) => void
}> = ({ rulePath, enabled, toggleRule }) => {
// Get the filename from the path for display
const displayName = rulePath.split("/").pop() || rulePath
const handleEditClick = () => {
vscode.postMessage({
type: "openFile",
text: rulePath,
})
}
return (
<div className="mb-2.5">
<div
className={`flex items-center p-2 rounded bg-[var(--vscode-textCodeBlock-background)] ${
enabled ? "opacity-100" : "opacity-60"
}`}>
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
{displayName}
</span>
{/* Toggle Switch */}
<div className="flex items-center ml-2 space-x-2">
<div
role="switch"
aria-checked={enabled}
tabIndex={0}
className={`w-[20px] h-[10px] rounded-[5px] relative cursor-pointer transition-colors duration-200 ${
enabled
? "bg-[var(--vscode-testing-iconPassed)] opacity-90"
: "bg-[var(--vscode-titleBar-inactiveForeground)] opacity-50"
}`}
onClick={() => toggleRule(rulePath, !enabled)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
toggleRule(rulePath, !enabled)
}
}}>
<div
className={`w-[6px] h-[6px] bg-white border border-[#66666699] rounded-full absolute top-[1px] transition-all duration-200 ${
enabled ? "left-[12px]" : "left-[2px]"
}`}
/>
</div>
<VSCodeButton
appearance="icon"
aria-label="Edit rule file"
title="Edit rule file"
onClick={handleEditClick}
style={{ height: "20px" }}>
<span className="codicon codicon-edit" style={{ fontSize: "14px" }} />
</VSCodeButton>
</div>
</div>
</div>
)
}
export default RuleRow
@@ -0,0 +1,31 @@
import RuleRow from "./RuleRow"
const RulesToggleList = ({
rules,
toggleRule,
listGap = "medium",
}: {
rules: [string, boolean][]
toggleRule: (rulePath: string, enabled: boolean) => void
listGap?: "small" | "medium" | "large"
}) => {
const gapClasses = {
small: "gap-0",
medium: "gap-2.5",
large: "gap-5",
}
const gapClass = gapClasses[listGap]
return rules.length > 0 ? (
<div className={`flex flex-col ${gapClass}`}>
{rules.map(([rulePath, enabled]) => (
<RuleRow key={rulePath} rulePath={rulePath} enabled={enabled} toggleRule={toggleRule} />
))}
</div>
) : (
<div className="flex flex-col items-center gap-3 my-5 text-[var(--vscode-descriptionForeground)]">No rules found</div>
)
}
export default RulesToggleList
@@ -162,7 +162,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
try {
const text = responseText || ""
const matches: UrlMatch[] = []
const urlRegex = /https?:\/\/[^\s<>"']+/g
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
let urlMatch: RegExpExecArray | null
let urlCount = 0

Some files were not shown because too many files have changed in this diff Show More