mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe9265c63a | |||
| 8d3981c618 | |||
| fb3105f7bf | |||
| bc87fdb4b3 | |||
| c021b6464b | |||
| 180ebdad74 | |||
| 2a80fedf7d | |||
| 450583c81d | |||
| 45b1666325 | |||
| 6c5b99d304 | |||
| 02120810ce | |||
| 612744394a | |||
| 01a48736eb | |||
| 4add38032e | |||
| 73078d63ce | |||
| 89cbbe95e3 | |||
| 4d696f377c | |||
| dd84bdaa9e | |||
| 4d8bdf2945 | |||
| ff3c840021 | |||
| 3cd2b18800 | |||
| 0b19ba6023 | |||
| 75143a718a | |||
| c69adcaf57 | |||
| 697887b7af | |||
| 138206eae9 | |||
| c5b714b59c | |||
| 7f60c08756 | |||
| ccaa999463 | |||
| 380b8c3aae | |||
| 9e94fd8e97 | |||
| ff31ac1e1a | |||
| 59210b8d5d | |||
| 30510dc73f | |||
| eee5b9b77a | |||
| fd46b9c54e | |||
| ee12bf5e30 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added chunking to terminal outputs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added copy button to MermaidBlock component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
gRPC over vscode message bus to make messaging better
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow user to modify a previous message to continue chat
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add command to focus chat input
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix vertexai token count
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow user to send context with an option selection
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
convert inline style to tailwind css of file `SettingsView.tsx`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Safer check for xAI reasoning content
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add stream options back to xai provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add an edit button to the Cline Rules toggle modal to open rule files
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
MCP ImageContent as tool result
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Icons to indicate an action is occuring outside of the users workspace
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Readme update
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add OpenAI o3 & 4o-mini
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add modal UI for toggling Cline Rules
|
||||
@@ -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 =
|
||||
@@ -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
@@ -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]
|
||||
|
||||
|
||||
@@ -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%" />
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
repositories
|
||||
|
||||
results/evals.db
|
||||
+162
@@ -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`
|
||||
Generated
+2455
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
`
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+164
-36
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+157
@@ -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)
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:;
|
||||
|
||||
@@ -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]) =>
|
||||
|
||||
@@ -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.`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
+23
-13
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user