mirror of
https://github.com/cline/cline.git
synced 2026-09-16 20:51:07 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf1f665de8 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
refreshRequestyModels protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove vite files and make enable all the first item
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
taskCompletionViewChanges protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
copyToClipboard protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adds switch to enabler/disable telemtry categories
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add detailed configuration options for LiteLLM provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Releasing memory after every diff edit to help fix grey screen webview crashes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate restartMcpServer to protobus
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remind VS code users of clines existence(Open Cline on AutoUpdate+KeyboardShortcuts+Lightbulb icons)
|
||||
@@ -209,7 +209,7 @@ class Task {
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
// Parse into content blocks
|
||||
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
|
||||
this.assistantMessageContent = parseAssistantMessage(chunk.text)
|
||||
// Present blocks to user
|
||||
await this.presentAssistantMessage()
|
||||
break
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ✨ Feature Request
|
||||
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
|
||||
|
||||
@@ -15,15 +15,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -68,11 +60,6 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
- name: Type Check
|
||||
run: npm run check-types
|
||||
|
||||
@@ -94,9 +81,8 @@ jobs:
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
# Run webview tests with coverage
|
||||
- name: Webview Tests with Coverage
|
||||
@@ -106,16 +92,13 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
npm run test:coverage > webview_coverage.txt 2>&1 || true
|
||||
cd ..
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
|
||||
# Save coverage reports as artifacts (workflow-scoped)
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
@@ -124,18 +107,14 @@ jobs:
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Print test results and check for failures
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
cat extension_coverage.txt
|
||||
cat webview-ui/webview_coverage.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
out
|
||||
dist
|
||||
dist-standalone
|
||||
node_modules
|
||||
tmp
|
||||
.vscode-test/
|
||||
@@ -19,4 +18,4 @@ coverage
|
||||
# But don't ignore the coverage scripts in .github/scripts/
|
||||
!.github/scripts/coverage/
|
||||
|
||||
*evals.env
|
||||
*evals.env
|
||||
+1
-2
@@ -3,6 +3,5 @@
|
||||
"useTabs": true,
|
||||
"printWidth": 130,
|
||||
"semi": false,
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf"
|
||||
"bracketSameLine": true
|
||||
}
|
||||
|
||||
Vendored
+1
-6
@@ -1,10 +1,5 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
|
||||
}
|
||||
|
||||
Vendored
-39
@@ -16,45 +16,6 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Fresh Install Mode)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--profile-temp",
|
||||
"--sync",
|
||||
"off",
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "clean-sandbox",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Standalone Extension",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"cwd": "${workspaceFolder}/dist-standalone",
|
||||
"outFiles": ["${workspaceFolder}/dist-standalone/**/*.js"],
|
||||
"preLaunchTask": "compile-standalone",
|
||||
"env": {
|
||||
"GRPC_TRACE": "all",
|
||||
"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
},
|
||||
"program": "standalone.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
-16
@@ -3,16 +3,6 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
@@ -195,12 +185,6 @@
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
},
|
||||
{
|
||||
"label": "clean-sandbox",
|
||||
"type": "shell",
|
||||
"dependsOn": ["watch"],
|
||||
"command": "rm -rf .vscode-dev"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -1,53 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.16.1]
|
||||
|
||||
- Add Enable auto approve toggle switch, allowing users to easily turn auto-approve functionality on or off without losing their action settings
|
||||
- Improve Gemini retry handling with better UI feedback, showing retry progress during API request attempts
|
||||
- Fix memory leak issue that could occur during long sessions with multiple tasks
|
||||
- Improve UI for Gemini model retry attempts with clearer status updates
|
||||
- Fix quick actions functionality in auto-approve settings
|
||||
- Update UI styling for auto-approve menu items to conserve space
|
||||
|
||||
## [3.16.0]
|
||||
|
||||
- Add new workflow feature allowing users to create and manage workflow files that can be injected into conversations via slash commands
|
||||
- Add collapsible recent task list, allowing users to hide their task history when sharing their screen (Thanks @cosmix!)
|
||||
- Add global endpoint option for Vertex AI users, providing higher availability and reducing 429 errors (Thanks @soniqua!)
|
||||
- Add detection for new users to display special components and guidance
|
||||
- Add Tailwind CSS IntelliSense to the recommended extensions list
|
||||
- Fix eternal loading states when the last message is a checkpoint (Thanks @BarreiroT!)
|
||||
- Improve settings organization by migrating VSCode Advanced settings to Settings Webview
|
||||
|
||||
## [3.15.5]
|
||||
|
||||
- Fix inefficient memory management in the task timeline
|
||||
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
|
||||
|
||||
## [3.15.4]
|
||||
|
||||
- Add gemini model back to vertex provider
|
||||
- Add gemini telemetry
|
||||
- Add filtering for tasks tied to the current workspace
|
||||
|
||||
## [3.15.3]
|
||||
|
||||
- Add Fireworks API Provider
|
||||
- Fix minor visual issues with auto-approve menu
|
||||
- Fix one instance of terminal not getting output
|
||||
- Fix 'Chrome was launched but debug port is not responding' error
|
||||
|
||||
## [3.15.2]
|
||||
|
||||
- Added details to auto approve menu and more sensible default controls
|
||||
- Add detailed configuration options for LiteLLM provider
|
||||
- Add webview telemetry for users who have opted in to telemetry
|
||||
- Update Gemini in OpenRouter/Cline providers to use implicit caching
|
||||
- Fix freezing issues during rendering of large streaming text
|
||||
- Fix grey screen webview crashes by releasing memory after every diff edit
|
||||
- Fix breaking out of diff auto-scroll
|
||||
- Fix IME composition Enter auto‑sending edited message
|
||||
|
||||
## [3.15.1]
|
||||
|
||||
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
|
||||
|
||||
@@ -44,16 +44,16 @@ This guide is tailored for organizations with established GCP environments (leve
|
||||
|
||||
#### 2.1 Choose and Confirm a Region
|
||||
|
||||
Vertex AI supports multiple regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
|
||||
Vertex AI supports eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
|
||||
|
||||
- **us-east5 (Columbus, Ohio)**
|
||||
- **us-east1 (South Carolina)**
|
||||
- **us-east4 (Northern Virginia)**
|
||||
- **us-central1 (Iowa)**
|
||||
- **us-west1 (The Dalles, Oregon)**
|
||||
- **us-west4 (Las Vegas, Nevada)**
|
||||
- **europe-west1 (Belgium)**
|
||||
- **europe-west4 (Netherlands)**
|
||||
- **asia-southeast1 (Singapore)**
|
||||
- **global (Global)**
|
||||
|
||||
The Global endpoint may offer higher availability and reduce resource exhausted errors. Only Gemini models are supported.
|
||||
|
||||
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
### How to Opt Out
|
||||
|
||||
Telemetry in Cline is entirely optional:
|
||||
Telemetry in Cline is entirely optional and requires your explicit consent:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
+5
-24
@@ -4,8 +4,6 @@ const path = require("path")
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
@@ -87,7 +85,7 @@ const copyWasmFiles = {
|
||||
build.onEnd(() => {
|
||||
// tree sitter
|
||||
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
|
||||
const targetDir = path.join(__dirname, destDir)
|
||||
const targetDir = path.join(__dirname, "dist")
|
||||
|
||||
// Copy tree-sitter.wasm
|
||||
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
|
||||
@@ -119,8 +117,7 @@ const copyWasmFiles = {
|
||||
},
|
||||
}
|
||||
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
const extensionConfig = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
@@ -143,32 +140,16 @@ const baseConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
entryPoints: ["src/extension.ts"],
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
const extensionConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/extension.ts"],
|
||||
outfile: `${destDir}/extension.js`,
|
||||
outfile: "dist/extension.js",
|
||||
external: ["vscode"],
|
||||
}
|
||||
|
||||
// Standalone-specific configuration
|
||||
const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/standalone.ts"],
|
||||
outfile: `${destDir}/standalone.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = standalone ? standaloneConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
const extensionCtx = await esbuild.context(extensionConfig)
|
||||
if (watch) {
|
||||
await extensionCtx.watch()
|
||||
} else {
|
||||
|
||||
Generated
+110
-1048
File diff suppressed because it is too large
Load Diff
+5
-33
@@ -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.16.1",
|
||||
"version": "3.15.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -40,8 +40,6 @@
|
||||
"llama"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage",
|
||||
"onStartupFinished",
|
||||
"workspaceContains:evals.env"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
@@ -126,16 +124,6 @@
|
||||
"title": "Generate Commit Message with Cline",
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
},
|
||||
{
|
||||
"command": "cline.explainCode",
|
||||
"title": "Explain with Cline",
|
||||
"category": "Cline"
|
||||
},
|
||||
{
|
||||
"command": "cline.improveCode",
|
||||
"title": "Improve with Cline",
|
||||
"category": "Cline"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
@@ -150,14 +138,6 @@
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "cline.focusChatInput",
|
||||
"key": "cmd+'",
|
||||
"mac": "cmd+'",
|
||||
"win": "ctrl+'",
|
||||
"linux": "ctrl+'",
|
||||
"when": "!editorHasSelection"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -260,14 +240,11 @@
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run protos && npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "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 && node scripts/generate-server-setup.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
@@ -310,8 +287,8 @@
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -337,9 +314,8 @@
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
@@ -350,7 +326,6 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
@@ -364,7 +339,6 @@
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
@@ -373,7 +347,6 @@
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open": "^10.1.2",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
@@ -387,7 +360,6 @@
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
+21
-34
@@ -29,7 +29,6 @@ const serviceNameMap = {
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
@@ -55,42 +54,30 @@ async function main() {
|
||||
|
||||
// Process all proto files
|
||||
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, absolute: true })
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const tsProtocCommand = [
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
|
||||
execSync(tsProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
for (const protoFile of protoFiles) {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
|
||||
|
||||
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const protocCommand = [
|
||||
protoc,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--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(" ")
|
||||
|
||||
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan("Generating descriptor set..."))
|
||||
execSync(descriptorProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating descriptor set for proto file:"), error)
|
||||
process.exit(1)
|
||||
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."))
|
||||
|
||||
@@ -8,9 +8,6 @@ import "common.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
// Copies text to clipboard
|
||||
rpc copyToClipboard(StringRequest) returns (Empty);
|
||||
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
@@ -25,9 +22,6 @@ service FileService {
|
||||
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
|
||||
// Select images from the file system and return as data URLs
|
||||
rpc selectImages(EmptyRequest) returns (StringArray);
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
@@ -88,7 +82,6 @@ message RuleFileRequest {
|
||||
bool is_global = 2; // Common field for all operations
|
||||
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
|
||||
optional string filename = 4; // Filename field for createRuleFile (optional)
|
||||
optional string type = 5; // Type of the file to create (optional)
|
||||
}
|
||||
|
||||
// Result for rule file operations with meaningful data only
|
||||
|
||||
@@ -10,8 +10,6 @@ service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
rpc restartMcpServer(StringRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// SlashService provides methods for managing slash
|
||||
service SlashService {
|
||||
// Sends button click message
|
||||
rpc reportBug(StringRequest) returns (Empty);
|
||||
rpc condense(StringRequest) returns (Empty);
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import "common.proto";
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
|
||||
@@ -25,12 +25,6 @@ service TaskService {
|
||||
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
// Sends a response to a previous ask operation
|
||||
rpc askResponse(AskResponseRequest) returns (Empty);
|
||||
// Records task feedback (thumbs up/down)
|
||||
rpc taskFeedback(StringRequest) returns (Empty);
|
||||
// Shows task completion changes diff in a view
|
||||
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -73,7 +67,6 @@ message GetTaskHistoryRequest {
|
||||
bool favorites_only = 2;
|
||||
string search_query = 3;
|
||||
string sort_by = 4;
|
||||
bool current_workspace_only = 5;
|
||||
}
|
||||
|
||||
// Response for task history
|
||||
@@ -95,11 +88,3 @@ message TaskItem {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
}
|
||||
|
||||
// Request for ask response operation
|
||||
message AskResponseRequest {
|
||||
Metadata metadata = 1;
|
||||
string response_type = 2;
|
||||
string text = 3;
|
||||
repeated string images = 4;
|
||||
}
|
||||
|
||||
@@ -8,19 +8,9 @@ import "common.proto";
|
||||
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
bool is_image = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message OpenGraphData {
|
||||
string title = 1;
|
||||
string description = 2;
|
||||
string image = 3;
|
||||
string url = 4;
|
||||
string site_name = 5;
|
||||
string type = 6;
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
import { fileURLToPath } from "url"
|
||||
import path from "path"
|
||||
|
||||
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
// Load service definitions.
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
function generateHandlersAndExports() {
|
||||
let imports = []
|
||||
let handlerSetup = []
|
||||
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (!def || !("service" in def)) {
|
||||
continue
|
||||
}
|
||||
const domain = name.replace(/Service$/, "")
|
||||
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
imports.push(`// ${domain} Service`)
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
|
||||
} else {
|
||||
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
handlerSetup.push(` });`)
|
||||
imports.push("")
|
||||
handlerSetup.push("")
|
||||
}
|
||||
return {
|
||||
imports: imports.join("\n"),
|
||||
handlerSetup: handlerSetup.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
const { imports, handlerSetup } = generateHandlersAndExports()
|
||||
const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
${imports}
|
||||
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
${handlerSetup}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
fs.writeFileSync(OUT_FILE, output)
|
||||
|
||||
console.log(`Generated service handlers in ${OUT_FILE}.`)
|
||||
@@ -1,96 +0,0 @@
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { Project, SyntaxKind } = require("ts-morph")
|
||||
|
||||
function traverse(container, output, prefix = "") {
|
||||
for (const node of container.getStatements()) {
|
||||
const kind = node.getKind()
|
||||
|
||||
if (kind === SyntaxKind.ModuleDeclaration) {
|
||||
const name = node.getName().replace(/^['"]|['"]$/g, "")
|
||||
var fullPrefix
|
||||
if (prefix) {
|
||||
fullPrefix = `${prefix}.${name}`
|
||||
} else {
|
||||
fullPrefix = name
|
||||
}
|
||||
output.push(`${fullPrefix} = {};`)
|
||||
const body = node.getBody()
|
||||
if (body && body.getKind() === SyntaxKind.ModuleBlock) {
|
||||
traverse(body, output, fullPrefix)
|
||||
}
|
||||
} else if (kind === SyntaxKind.FunctionDeclaration) {
|
||||
const name = node.getName()
|
||||
const params = node.getParameters().map((p, i) => sanitizeParam(p.getName(), i))
|
||||
const typeNode = node.getReturnTypeNode()
|
||||
const returnType = typeNode ? typeNode.getText() : ""
|
||||
const ret = mapReturn(returnType)
|
||||
output.push(
|
||||
`${prefix}.${name} = function(${params.join(", ")}) { console.log('Called stubbed function: ${prefix}.${name}'); ${ret} };`,
|
||||
)
|
||||
} else if (kind === SyntaxKind.EnumDeclaration) {
|
||||
const name = node.getName()
|
||||
const members = node.getMembers().map((m) => m.getName())
|
||||
output.push(`${prefix}.${name} = { ${members.map((m) => `${m}: 0`).join(", ")} };`)
|
||||
} else if (kind === SyntaxKind.VariableStatement) {
|
||||
for (const decl of node.getDeclarations()) {
|
||||
const name = decl.getName()
|
||||
output.push(`${prefix}.${name} = createStub("${prefix}.${name}");`)
|
||||
}
|
||||
} else if (kind == SyntaxKind.ClassDeclaration) {
|
||||
const name = node.getName()
|
||||
output.push(
|
||||
`${prefix}.${name} = class { constructor(...args) {
|
||||
console.log('Constructed stubbed class: new ${prefix}.${name}(', args, ')');
|
||||
return createStub(${prefix}.${name});
|
||||
}};`,
|
||||
)
|
||||
} else if (kind === SyntaxKind.TypeAliasDeclaration || kind === SyntaxKind.InterfaceDeclaration) {
|
||||
//console.log("Skipping", SyntaxKind[kind], node.getName())
|
||||
// Skip interfaces and type aliases because they are only used at compile time by typescript.
|
||||
} else {
|
||||
console.log("Can't handle: ", SyntaxKind[kind])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapReturn(typeStr) {
|
||||
if (!typeStr) return ""
|
||||
if (typeStr.includes("void")) return ""
|
||||
if (typeStr.includes("string")) return `return '';`
|
||||
if (typeStr.includes("number")) return `return 0;`
|
||||
if (typeStr.includes("boolean")) return `return false;`
|
||||
if (typeStr.includes("[]")) return `return [];`
|
||||
if (typeStr.includes("Thenable")) return `return Promise.resolve(null);`
|
||||
return `return createStub("unknown");`
|
||||
}
|
||||
|
||||
function sanitizeParam(name, index) {
|
||||
return name || `arg${index}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const inputPath = "node_modules/@types/vscode/index.d.ts"
|
||||
const outputPath = "standalone/runtime-files/vscode/vscode-stubs.js"
|
||||
|
||||
const project = new Project()
|
||||
const sourceFile = project.addSourceFileAtPath(inputPath)
|
||||
|
||||
const output = []
|
||||
output.push("// GENERATED CODE -- DO NOT EDIT!")
|
||||
output.push('console.log("Loading stubs...");')
|
||||
output.push('const { createStub } = require("./stub-utils")')
|
||||
traverse(sourceFile, output)
|
||||
output.push("module.exports = vscode;")
|
||||
output.push('console.log("Finished loading stubs");')
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
||||
fs.writeFileSync(outputPath, output.join("\n"))
|
||||
|
||||
console.log(`Wrote vscode SDK stubs to ${outputPath}`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
DEST=dist-standalone/vscode-uses.txt
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
sort | uniq > $DEST
|
||||
}
|
||||
|
||||
echo Done, wrote uses of the vscode SDK to $(realpath $DEST)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { glob } from "glob"
|
||||
import archiver from "archiver"
|
||||
import { cp } from "fs/promises"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const SOURCE_DIR = "standalone/runtime-files"
|
||||
|
||||
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
|
||||
|
||||
// Run npm install in the distribution directory
|
||||
console.log("Running npm install in distribution directory...")
|
||||
const cwd = process.cwd()
|
||||
process.chdir(BUILD_DIR)
|
||||
try {
|
||||
execSync("npm install", { stdio: "inherit" })
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which is not portable.
|
||||
fs.renameSync("vscode", path.join("node_modules", "vscode"))
|
||||
} catch (error) {
|
||||
console.error("Error during setup:", error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
process.chdir(cwd)
|
||||
}
|
||||
|
||||
// Check for native .node modules.
|
||||
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
|
||||
if (nativeModules.length > 0) {
|
||||
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Zip the build directory (excluding any pre-existing output zip).
|
||||
const zipPath = path.join(BUILD_DIR, "standalone.zip")
|
||||
const output = fs.createWriteStream(zipPath)
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
|
||||
})
|
||||
|
||||
archive.on("error", (err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
archive.pipe(output)
|
||||
archive.glob("**/*", {
|
||||
cwd: BUILD_DIR,
|
||||
ignore: ["standalone.zip"],
|
||||
})
|
||||
await archive.finalize()
|
||||
+1
-1
@@ -9,7 +9,7 @@ try {
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test:coverage", { stdio: "inherit" })
|
||||
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
|
||||
@@ -19,7 +19,6 @@ import { DoubaoHandler } from "./providers/doubao"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
import { FireworksHandler } from "./providers/fireworks"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
@@ -59,8 +58,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new DeepSeekHandler(options)
|
||||
case "requesty":
|
||||
return new RequestyHandler(options)
|
||||
case "fireworks":
|
||||
return new FireworksHandler(options)
|
||||
case "together":
|
||||
return new TogetherHandler(options)
|
||||
case "qwen":
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from ".."
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
DeepSeekModelId,
|
||||
ModelInfo,
|
||||
deepSeekDefaultModelId,
|
||||
deepSeekModels,
|
||||
openAiModelInfoSaneDefaults,
|
||||
} from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class FireworksHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.fireworksModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
...(this.options.fireworksModelMaxCompletionTokens
|
||||
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
|
||||
: {}),
|
||||
...(this.options.fireworksModelMaxTokens ? { max_tokens: this.options.fireworksModelMaxTokens } : {}),
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (reasoning || delta?.content?.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + (delta.content ?? "")
|
||||
}
|
||||
|
||||
if (delta?.content && !reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
|
||||
}
|
||||
if (reasoning?.includes("</think>")) {
|
||||
// Reset so the next chunk is regular content
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.fireworksModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-108
@@ -1,12 +1,11 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
@@ -73,13 +72,9 @@ export class GeminiHandler implements ApiHandler {
|
||||
* @param messages The conversation history to include in the message
|
||||
* @returns An async generator that yields chunks of the response with accurate immediate costs
|
||||
*/
|
||||
@withRetry({
|
||||
maxRetries: 4,
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const { id: model, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Configure thinking budget if supported
|
||||
@@ -103,110 +98,52 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Generate content using the configured parameters
|
||||
const sdkCallStartTime = Date.now()
|
||||
let sdkFirstChunkTime: number | undefined
|
||||
let ttftSdkMs: number | undefined
|
||||
let apiSuccess = false
|
||||
let apiError: string | undefined
|
||||
let promptTokens = 0
|
||||
let outputTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
// Track usage metadata
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
try {
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
// Process the stream
|
||||
for await (const chunk of result) {
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// Yield usage information at the end
|
||||
if (lastUsageMetadata) {
|
||||
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
|
||||
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
|
||||
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
|
||||
|
||||
// Calculate immediate costs
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
})
|
||||
|
||||
let isFirstSdkChunk = true
|
||||
for await (const chunk of result) {
|
||||
if (isFirstSdkChunk) {
|
||||
sdkFirstChunkTime = Date.now()
|
||||
ttftSdkMs = sdkFirstChunkTime - sdkCallStartTime
|
||||
isFirstSdkChunk = false
|
||||
}
|
||||
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
|
||||
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
}
|
||||
}
|
||||
apiSuccess = true
|
||||
|
||||
if (lastUsageMetadata) {
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
apiSuccess = false
|
||||
// Let the error propagate to be handled by withRetry or Task.ts
|
||||
// Telemetry will be sent in the finally block.
|
||||
if (error instanceof Error) {
|
||||
apiError = error.message
|
||||
|
||||
// Gemini doesn't include status codes in their errors
|
||||
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
|
||||
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
|
||||
;(error as any).status = 429
|
||||
}
|
||||
} else {
|
||||
apiError = String(error)
|
||||
}
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
const sdkCallEndTime = Date.now()
|
||||
const totalDurationSdkMs = sdkCallEndTime - sdkCallStartTime
|
||||
const cacheHit = cacheReadTokens > 0
|
||||
const cacheHitPercentage = promptTokens > 0 ? (cacheReadTokens / promptTokens) * 100 : undefined
|
||||
const throughputTokensPerSecSdk =
|
||||
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
|
||||
|
||||
if (this.options.taskId) {
|
||||
telemetryService.captureGeminiApiPerformance(
|
||||
this.options.taskId,
|
||||
modelId,
|
||||
{
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheHit,
|
||||
cacheHitPercentage,
|
||||
apiSuccess,
|
||||
apiError,
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,15 +54,6 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
|
||||
}
|
||||
|
||||
const handlerInstance = this as any
|
||||
if (handlerInstance.options?.onRetryAttempt) {
|
||||
try {
|
||||
handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,74 @@ export async function createOpenRouterStream(
|
||||
break
|
||||
}
|
||||
|
||||
// handles gemini caching logic
|
||||
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
|
||||
// gemini only uses the last breakpoint for caching, so the others will be ignored
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// for safety, but this should always be the case
|
||||
if (openAiMessages.length >= 2) {
|
||||
const msg = openAiMessages[1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
|
||||
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
|
||||
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
|
||||
|
||||
const userMessageCount = userMessages.length
|
||||
const targetUserMessageNumber =
|
||||
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
|
||||
|
||||
if (targetUserMessageNumber > 0) {
|
||||
// otherwise dont need to add a breakpoint
|
||||
const msg = userMessages[targetUserMessageNumber - 1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
export { parseAssistantMessage } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
|
||||
|
||||
/**
|
||||
* @description **Version 1**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version iterates through the message character by character, building an accumulator string.
|
||||
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
|
||||
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
|
||||
* the corresponding opening or closing tags.
|
||||
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
|
||||
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
|
||||
* occurrence of the closing tag.
|
||||
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
|
||||
export function parseAssistantMessage(assistantMessage: string) {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
@@ -32,56 +14,46 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
|
||||
const char = assistantMessage[i]
|
||||
accumulator += char
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
// there should not be a param without a tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
|
||||
const paramClosingTag = `</${currentParamName}>`
|
||||
if (currentParamValue.endsWith(paramClosingTag)) {
|
||||
// End of param value found
|
||||
// end of param value
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
currentParamName = undefined
|
||||
continue
|
||||
} else {
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
// partial param value is accumulating
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
// no currentParamName
|
||||
|
||||
if (currentToolUse) {
|
||||
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
|
||||
const toolUseClosingTag = `</${currentToolUse.name}>`
|
||||
|
||||
if (currentToolValue.endsWith(toolUseClosingTag)) {
|
||||
// End of a tool use found
|
||||
// end of a tool use
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Go back to parsing text or looking for next tool
|
||||
// Reset text start index in case text follows immediately
|
||||
currentTextContentStartIndex = i + 1
|
||||
continue // Move to next character
|
||||
currentToolUse = undefined
|
||||
continue
|
||||
} else {
|
||||
// Check if starting a new parameter within the current tool use
|
||||
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
|
||||
let foundParamStart = false
|
||||
for (const paramOpeningTag of possibleParamOpeningTags) {
|
||||
if (accumulator.endsWith(paramOpeningTag)) {
|
||||
// Start of a new parameter found
|
||||
// start of a new parameter
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// Special case for write_to_file/new_rule content param allowing nested tags
|
||||
// Check if a </content> tag appears, potentially indicating the end of the content param
|
||||
// even if the main tool closing tag hasn't been seen yet.
|
||||
// there's no current param, and not starting a new param
|
||||
|
||||
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
@@ -91,384 +63,73 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
|
||||
// Use lastIndexOf to handle cases where </content> might appear within the content itself
|
||||
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
|
||||
|
||||
// Ensure we found valid start/end tags and end is after start
|
||||
if (
|
||||
contentStartIndex !== -1 &&
|
||||
contentEndIndex !== -1 &&
|
||||
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
|
||||
) {
|
||||
// Check if this content param was already being parsed. If so, update it.
|
||||
// If not, and we just found the closing tag, assign it.
|
||||
// This handles cases where the </content> detection might fire before
|
||||
// the <content> tag detection logic, or if the content is very short.
|
||||
if (currentParamName === contentParamName) {
|
||||
// Already parsing content, now we found the end tag
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
currentParamName = undefined // Finished with this param
|
||||
} else if (currentParamName === undefined) {
|
||||
// Not parsing a param, but found </content>. Assume it closes the content block.
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
|
||||
}
|
||||
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
// partial tool value is accumulating
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing Text (or looking for start of a tool use) ---
|
||||
// no currentToolUse
|
||||
|
||||
let didStartToolUse = false
|
||||
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
|
||||
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
|
||||
if (accumulator.endsWith(toolUseOpeningTag)) {
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
// start of a new tool use
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
// this also indicates the end of the current text content
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// Extract text content, removing the part that formed the tool opening tag
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
// Only add if there's actual content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
// remove the partially accumulated tool use tag from the end of text (<tool)
|
||||
currentTextContent.content = currentTextContent.content
|
||||
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
|
||||
.trim()
|
||||
contentBlocks.push(currentTextContent)
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check if there was text before this tool use started
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false, // Ended because tool use started
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didStartToolUse = true
|
||||
break // Found tool start, stop checking for others
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
// no tool use, so it must be text either at the beginning or between tools
|
||||
if (currentTextContent === undefined) {
|
||||
// Start of a new text block
|
||||
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
|
||||
// If accumulator starts from 0, start index is i
|
||||
if (contentBlocks.length === 0 && currentToolUse === undefined) {
|
||||
currentTextContentStartIndex = accumulator.length - 1 // i
|
||||
} else {
|
||||
// Re-calculate based on the actual start of the current text segment
|
||||
// Find the end of the last block
|
||||
let lastBlockEndIndex = 0
|
||||
if (contentBlocks.length > 0) {
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
|
||||
// Simpler: Assume text starts right after the last block ended implicitly at index i.
|
||||
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
|
||||
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
|
||||
// Let's stick to the accumulator slice approach for simplicity in this version.
|
||||
// The start index should be where the current *unmatched* text began.
|
||||
let lastProcessedIndex = -1
|
||||
if (contentBlocks.length > 0) {
|
||||
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
|
||||
// We'll approximate based on the current accumulator and start index logic.
|
||||
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
|
||||
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
|
||||
}
|
||||
// Reset start index to the beginning of the *current* potential text block
|
||||
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
|
||||
}
|
||||
|
||||
// If we just closed a tool, text starts *after* its closing tag
|
||||
// The logic needs refinement here for accurate start index after a tool closure.
|
||||
// Let's assume for now the start index logic inside the loop handles it via slicing.
|
||||
}
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Content will be filled by slicing accumulator
|
||||
partial: true,
|
||||
}
|
||||
currentTextContentStartIndex = i
|
||||
}
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: accumulator.slice(currentTextContentStartIndex).trim(),
|
||||
partial: true,
|
||||
}
|
||||
// Update text content based on the accumulator from its start index
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
|
||||
}
|
||||
} // End of loop
|
||||
}
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// If a tool use was open at the end
|
||||
if (currentToolUse) {
|
||||
// If a parameter was open within that tool use
|
||||
// stream did not complete tool call, add it as partial
|
||||
if (currentParamName) {
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
// tool call has a parameter that was not completed
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// If text content was being accumulated at the end
|
||||
// Note: Only one of currentToolUse or currentTextContent can be defined here,
|
||||
// as starting a tool use finalizes the preceding text block.
|
||||
else if (currentTextContent) {
|
||||
// Update content one last time
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
|
||||
// Add the potentially partial text block only if it contains content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
|
||||
* It iterates through the string using an index `i`. At each position, it checks if the substring
|
||||
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
|
||||
* with an offset.
|
||||
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
|
||||
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
|
||||
* pointing to the start of the current block within the original `assistantMessage` string.
|
||||
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
|
||||
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
|
||||
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
|
||||
* If the input string ends mid-block, the last open block is added and marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContentStart = 0 // Index where the current text block started
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
|
||||
// Precompute tags for faster lookups
|
||||
const toolUseOpenTags = new Map<string, ToolUseName>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
for (const name of toolUseNames) {
|
||||
toolUseOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
for (const name of toolParamNames) {
|
||||
toolParamOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
const len = assistantMessage.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
const currentCharIndex = i
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
if (currentToolUse && currentParamName) {
|
||||
const closeTag = `</${currentParamName}>`
|
||||
// Check if the string *ending* at index `i` matches the closing tag
|
||||
if (
|
||||
currentCharIndex >= closeTag.length - 1 &&
|
||||
assistantMessage.startsWith(
|
||||
closeTag,
|
||||
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
|
||||
)
|
||||
) {
|
||||
// Found the closing tag for the parameter
|
||||
const value = assistantMessage
|
||||
.slice(
|
||||
currentParamValueStart, // Start after the opening tag
|
||||
currentCharIndex - closeTag.length + 1, // End before the closing tag
|
||||
)
|
||||
.trim()
|
||||
currentToolUse.params[currentParamName] = value
|
||||
currentParamName = undefined // Go back to parsing tool content
|
||||
// We don't continue loop here, need to check for tool close or other params at index i
|
||||
} else {
|
||||
continue // Still inside param value, move to next char
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
if (currentToolUse && !currentParamName) {
|
||||
// Ensure we are not inside a parameter already
|
||||
// Check if starting a new parameter
|
||||
let startedNewParam = false
|
||||
for (const [tag, paramName] of toolParamOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
currentParamName = paramName
|
||||
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
|
||||
startedNewParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (startedNewParam) {
|
||||
continue // Handled start of param, move to next char
|
||||
}
|
||||
|
||||
// Check if closing the current tool use
|
||||
const toolCloseTag = `</${currentToolUse.name}>`
|
||||
if (
|
||||
currentCharIndex >= toolCloseTag.length - 1 &&
|
||||
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
|
||||
) {
|
||||
// End of the tool use found
|
||||
// Special handling for content params *before* finalizing the tool
|
||||
const toolContentSlice = assistantMessage.slice(
|
||||
currentToolUseStart, // From after the tool opening tag
|
||||
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
|
||||
)
|
||||
|
||||
// Check if content parameter needs special handling (write_to_file/new_rule)
|
||||
// This check is important if the closing </content> tag was missed by the parameter parsing logic
|
||||
// (e.g., if content is empty or parsing logic prioritizes tool close)
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
|
||||
toolContentSlice.includes(`<${contentParamName}>`)
|
||||
) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStart = toolContentSlice.indexOf(contentStartTag)
|
||||
// Use lastIndexOf for robustness against nested tags
|
||||
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
|
||||
|
||||
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
|
||||
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
|
||||
currentToolUse.params[contentParamName] = contentValue
|
||||
}
|
||||
}
|
||||
|
||||
currentToolUse.partial = false // Mark as complete
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Reset state
|
||||
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
|
||||
continue // Move to next char
|
||||
}
|
||||
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
|
||||
continue
|
||||
}
|
||||
|
||||
// --- State: Parsing Text / Looking for Tool Start ---
|
||||
if (!currentToolUse) {
|
||||
// Check if starting a new tool use
|
||||
let startedNewTool = false
|
||||
for (const [tag, toolName] of toolUseOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
// End current text block if one was active
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
currentTextContent.partial = false // Ended because tool started
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check for any text between the last block and this tag
|
||||
const potentialText = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text *might* have started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new tool use
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true, // Assume partial until closing tag is found
|
||||
}
|
||||
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
|
||||
startedNewTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewTool) {
|
||||
continue // Handled start of tool, move to next char
|
||||
}
|
||||
|
||||
// If not starting a tool, it must be text content
|
||||
if (!currentTextContent) {
|
||||
// Start a new text block if we aren't already in one
|
||||
currentTextContentStart = currentCharIndex // Text starts at the current character
|
||||
// Check if the current char is the start of potential text *immediately* after a tag
|
||||
// This needs the previous state - simpler to let slicing handle it later.
|
||||
// Resetting start index accurately is key.
|
||||
// It should be the index *after* the last processed tag.
|
||||
// The logic managing currentTextContentStart after closing tags handles this.
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Will be determined by slicing at the end or when a tool starts
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Continue accumulating text implicitly; content is extracted later.
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// Finalize any open parameter within an open tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
currentToolUse.params[currentParamName] = assistantMessage
|
||||
.slice(currentParamValueStart) // From param start to end of string
|
||||
.trim()
|
||||
// Tool use remains partial
|
||||
}
|
||||
|
||||
// Finalize any open tool use (which might contain the finalized partial param)
|
||||
if (currentToolUse) {
|
||||
// Tool use is partial because the loop finished before its closing tag
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// Finalize any trailing text content
|
||||
// Only possible if a tool use wasn't open at the very end
|
||||
else if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart) // From text start to end of string
|
||||
.trim()
|
||||
// Text is partial because the loop finished
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
|
||||
@@ -8,6 +8,45 @@ import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceSt
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
|
||||
* Doesn't do anything if .clinerules dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
|
||||
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
const defaultRuleFilename = "default-rules.md"
|
||||
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
if (exists && !(await isDirectory(clinerulePath))) {
|
||||
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
|
||||
const content = await fs.readFile(clinerulePath, "utf8")
|
||||
const tempPath = clinerulePath + ".bak"
|
||||
await fs.rename(clinerulePath, tempPath) // create backup
|
||||
try {
|
||||
await fs.mkdir(clinerulePath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
|
||||
await fs.unlink(tempPath).catch(() => {}) // delete backup
|
||||
|
||||
return false // conversion successful with no errors
|
||||
} catch (conversionError) {
|
||||
// attempt to restore backup on conversion failure
|
||||
try {
|
||||
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
|
||||
await fs.rename(tempPath, clinerulePath) // restore backup
|
||||
} catch (restoreError) {}
|
||||
return true // in either case here we consider this an error
|
||||
}
|
||||
}
|
||||
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
|
||||
return false
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
if (await isDirectory(globalClineRulesFilePath)) {
|
||||
@@ -41,8 +80,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
|
||||
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath)
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
@@ -83,9 +121,7 @@ export async function refreshClineRulesToggles(
|
||||
// Local toggles
|
||||
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
])
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
@@ -93,3 +129,82 @@ export async function refreshClineRulesToggles(
|
||||
localToggles: updatedLocalToggles,
|
||||
}
|
||||
}
|
||||
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClinerulesDirExists(cwd)
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Rule file does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Rule file "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting rule file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete rule file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
*/
|
||||
export async function readDirectoryRecursive(
|
||||
directoryPath: string,
|
||||
allowedFileExtension: string,
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<string[]> {
|
||||
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readDirectory(directoryPath, excludedPaths)
|
||||
const entries = await readDirectory(directoryPath)
|
||||
let results: string[] = []
|
||||
for (const entry of entries) {
|
||||
if (allowedFileExtension !== "") {
|
||||
@@ -40,7 +33,6 @@ export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
@@ -53,7 +45,7 @@ export async function synchronizeRuleToggles(
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
@@ -127,155 +119,3 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
|
||||
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
|
||||
* Doesn't do anything if the dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
if (exists && !(await isDirectory(clinerulePath))) {
|
||||
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
|
||||
const content = await fs.readFile(clinerulePath, "utf8")
|
||||
const tempPath = clinerulePath + ".bak"
|
||||
await fs.rename(clinerulePath, tempPath) // create backup
|
||||
try {
|
||||
await fs.mkdir(clinerulePath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
|
||||
await fs.unlink(tempPath).catch(() => {}) // delete backup
|
||||
|
||||
return false // conversion successful with no errors
|
||||
} catch (conversionError) {
|
||||
// attempt to restore backup on conversion failure
|
||||
try {
|
||||
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
|
||||
await fs.rename(tempPath, clinerulePath) // restore backup
|
||||
} catch (restoreError) {}
|
||||
return true // in either case here we consider this an error
|
||||
}
|
||||
}
|
||||
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
|
||||
return false
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rule file or workflow file
|
||||
*/
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string, type: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
// global means its implicitly clinerules
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localClineRulesFilePath, "default-rules.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
if (type === "workflow") {
|
||||
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localWorkflowsFilePath, filename)
|
||||
} else {
|
||||
// clinerules file creation
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a rule file or workflow file
|
||||
*/
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
type: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `File does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "workflowToggles", toggles)
|
||||
} else if (type === "cursor") {
|
||||
const toggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localCursorRulesToggles", toggles)
|
||||
} else if (type === "windsurf") {
|
||||
const toggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localWindsurfRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Refresh the workflow toggles
|
||||
*/
|
||||
export async function refreshWorkflowToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<ClineRulesToggles> {
|
||||
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
||||
return updatedWorkflowToggles
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Copies text to the system clipboard
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the text to copy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import {
|
||||
createRuleFile as createRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { cwd } from "@core/task"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
@@ -17,45 +18,32 @@ import { refreshWorkflowToggles } from "@/core/context/instructions/user-instruc
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
!request.filename ||
|
||||
typeof request.filename !== "string" ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
|
||||
console.error("createRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error("Failed to create file.")
|
||||
throw new Error("Failed to create rule file.")
|
||||
}
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
if (request.type === "workflow") {
|
||||
await refreshWorkflowToggles(controller.context, cwd)
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
}
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import {
|
||||
deleteRuleFile as deleteRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { cwd } from "@core/task"
|
||||
@@ -17,38 +18,26 @@ import { cwd } from "@core/task"
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
typeof request.rulePath !== "string" ||
|
||||
!request.rulePath ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
|
||||
console.error("deleteRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Failed to delete rule file")
|
||||
}
|
||||
|
||||
// we refresh inside of the deleteRuleFileImpl(..) call
|
||||
//await refreshClineRulesToggles(controller.context, cwd)
|
||||
//await refreshExternalRulesToggles(controller.context, cwd)
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await refreshExternalRulesToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { copyToClipboard } from "./copyToClipboard"
|
||||
import { createRuleFile } from "./createRuleFile"
|
||||
import { deleteRuleFile } from "./deleteRuleFile"
|
||||
import { getRelativePaths } from "./getRelativePaths"
|
||||
@@ -11,12 +10,10 @@ import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
import { searchFiles } from "./searchFiles"
|
||||
import { selectImages } from "./selectImages"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("copyToClipboard", copyToClipboard)
|
||||
registerMethod("createRuleFile", createRuleFile)
|
||||
registerMethod("deleteRuleFile", deleteRuleFile)
|
||||
registerMethod("getRelativePaths", getRelativePaths)
|
||||
@@ -24,5 +21,4 @@ export function registerAllMethods(): void {
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
registerMethod("searchFiles", searchFiles)
|
||||
registerMethod("selectImages", selectImages)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest, StringArray } from "@shared/proto/common"
|
||||
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Prompts the user to select images from the file system and returns them as data URLs
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request, no parameters needed
|
||||
* @returns Array of image data URLs
|
||||
*/
|
||||
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
|
||||
try {
|
||||
const images = await selectImagesIntegration()
|
||||
return StringArray.create({ values: images })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
// Return empty array on error
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
|
||||
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
|
||||
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
|
||||
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
|
||||
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
|
||||
|
||||
/**
|
||||
* Configuration for a service handler
|
||||
@@ -68,8 +67,4 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
|
||||
requestHandler: handleModelsServiceRequest,
|
||||
streamingHandler: handleModelsServiceStreamingRequest,
|
||||
},
|
||||
"cline.SlashService": {
|
||||
requestHandler: handleSlashServiceRequest,
|
||||
streamingHandler: handleSlashServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
|
||||
+213
-34
@@ -14,6 +14,7 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
|
||||
import { handleFileServiceRequest } from "./file"
|
||||
import { selectImages } from "@integrations/misc/process-images"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
@@ -50,7 +51,6 @@ import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -66,7 +66,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "may-16-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -146,18 +146,8 @@ export class Controller {
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
|
||||
await updateGlobalState(this.context, "isNewUser", false)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
if (autoApprovalSettings) {
|
||||
const updatedAutoApprovalSettings = {
|
||||
...autoApprovalSettings,
|
||||
@@ -261,7 +251,7 @@ export class Controller {
|
||||
// If user already opted in to telemetry, enable telemetry service
|
||||
this.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
break
|
||||
@@ -283,6 +273,12 @@ export class Controller {
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initTask(message.text, message.images)
|
||||
break
|
||||
case "condense":
|
||||
this.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "reportBug":
|
||||
this.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
await updateApiConfiguration(this.context, message.apiConfiguration)
|
||||
@@ -323,14 +319,26 @@ export class Controller {
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
await browserSession.relaunchChromeDebugMode(this)
|
||||
break
|
||||
case "askResponse":
|
||||
this.task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||
break
|
||||
case "didShowAnnouncement":
|
||||
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "selectImages":
|
||||
const images = await selectImages()
|
||||
await this.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
await refreshWorkflowToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openInBrowser":
|
||||
@@ -338,9 +346,18 @@ export class Controller {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "fetchOpenGraphData":
|
||||
this.fetchOpenGraphData(message.text!)
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.task?.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "accountLogoutClicked": {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
@@ -364,10 +381,28 @@ export class Controller {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
case "downloadMcp": {
|
||||
if (message.mcpId) {
|
||||
// 1. Toggle to act mode if we are in plan mode
|
||||
const { chatSettings } = await this.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await this.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// 2. download MCP
|
||||
await this.downloadMcp(message.mcpId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "silentlyRefreshMcpMarketplace": {
|
||||
await this.silentlyRefreshMcpMarketplace()
|
||||
break
|
||||
}
|
||||
case "taskFeedback":
|
||||
if (message.feedbackType && this.task?.taskId) {
|
||||
telemetryService.captureTaskFeedback(this.task.taskId, message.feedbackType)
|
||||
}
|
||||
break
|
||||
// case "openMcpMarketplaceServerDetails": {
|
||||
// if (message.text) {
|
||||
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
|
||||
@@ -468,20 +503,18 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleWorkflow": {
|
||||
const { workflowPath, enabled } = message
|
||||
if (workflowPath && typeof enabled === "boolean") {
|
||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
toggles[workflowPath] = enabled
|
||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
if (message.serverName) {
|
||||
this.mcpHub?.deleteServer(message.serverName)
|
||||
@@ -594,6 +627,27 @@ export class Controller {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
}
|
||||
case "toggleFavoriteModel": {
|
||||
if (message.modelId) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(message.modelId)
|
||||
? favoritedModelIds.filter((id) => id !== message.modelId)
|
||||
: [...favoritedModelIds, message.modelId]
|
||||
|
||||
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(message.modelId)
|
||||
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
|
||||
|
||||
// Post state to webview without changing any other configuration
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this, message.grpc_request)
|
||||
@@ -607,6 +661,14 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
|
||||
case "copyToClipboard": {
|
||||
try {
|
||||
await vscode.env.clipboard.writeText(message.text || "")
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "updateTerminalConnectionTimeout": {
|
||||
if (message.shellIntegrationTimeout !== undefined) {
|
||||
const timeout = message.shellIntegrationTimeout
|
||||
@@ -629,7 +691,7 @@ export class Controller {
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
}
|
||||
|
||||
@@ -983,6 +1045,92 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadMcp(mcpId: string) {
|
||||
try {
|
||||
// First check if we already have this MCP server installed
|
||||
const servers = this.mcpHub?.getServers() || []
|
||||
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
|
||||
|
||||
if (isInstalled) {
|
||||
throw new Error("This MCP server is already installed")
|
||||
}
|
||||
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
console.log("[downloadMcp] Response from download API", { response })
|
||||
|
||||
const mcpDetails = response.data
|
||||
|
||||
// Validate required fields
|
||||
if (!mcpDetails.githubUrl) {
|
||||
throw new Error("Missing GitHub URL in MCP download response")
|
||||
}
|
||||
if (!mcpDetails.readmeContent) {
|
||||
throw new Error("Missing README content in MCP download response")
|
||||
}
|
||||
|
||||
// Send details to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
mcpDownloadDetails: mcpDetails,
|
||||
})
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initTask(task)
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to download MCP:", error)
|
||||
let errorMessage = "Failed to download MCP"
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = "MCP server not found in marketplace."
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Internal server error. Please try again later."
|
||||
} else if (!error.response && error.request) {
|
||||
errorMessage = "Network error. Please check your internet connection."
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Show error in both notification and marketplace UI
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
|
||||
async handleOpenRouterCallback(code: string) {
|
||||
@@ -1312,10 +1460,7 @@ export class Controller {
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
// For testing: Bypass gRPC stream and send state directly
|
||||
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
|
||||
await this.postMessageToWebview({ type: "state", state: state })
|
||||
// await sendStateUpdate(state) // Original line for the GrPC stream
|
||||
await sendStateUpdate(state)
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -1334,7 +1479,6 @@ export class Controller {
|
||||
enableCheckpointsSetting,
|
||||
globalClineRulesToggles,
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1346,8 +1490,6 @@ export class Controller {
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
apiConfiguration,
|
||||
@@ -1375,9 +1517,7 @@ export class Controller {
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
workflowToggles: workflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1453,6 +1593,30 @@ export class Controller {
|
||||
|
||||
// secrets
|
||||
|
||||
// Open Graph Data
|
||||
|
||||
async fetchOpenGraphData(url: string) {
|
||||
try {
|
||||
// Use the fetchOpenGraphData function from link-preview.ts
|
||||
const ogData = await fetchOpenGraphData(url)
|
||||
|
||||
// Send the data back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
openGraphData: ogData,
|
||||
url: url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data for ${url}:`, error)
|
||||
// Send an error response
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
error: `Failed to fetch Open Graph data: ${error}`,
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Git commit message generation
|
||||
|
||||
async generateGitCommitMessage() {
|
||||
@@ -1557,4 +1721,19 @@ Commit message:`
|
||||
}
|
||||
|
||||
// dev
|
||||
|
||||
async resetState() {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(this.context)
|
||||
if (this.task) {
|
||||
this.task.abortTask()
|
||||
this.task = undefined
|
||||
}
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { McpServer, McpDownloadResponse } from "@shared/mcp"
|
||||
import axios from "axios"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Download an MCP server from the marketplace
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the MCP ID
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
// Check if mcpId is provided
|
||||
if (!request.value) {
|
||||
throw new Error("MCP ID is required")
|
||||
}
|
||||
|
||||
const mcpId = request.value
|
||||
|
||||
// Check if we already have this MCP server installed
|
||||
const servers = controller.mcpHub?.getServers() || []
|
||||
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
|
||||
|
||||
if (isInstalled) {
|
||||
throw new Error("This MCP server is already installed")
|
||||
}
|
||||
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
console.log("[downloadMcp] Response from download API", { response })
|
||||
|
||||
const mcpDetails = response.data
|
||||
|
||||
// Validate required fields
|
||||
if (!mcpDetails.githubUrl) {
|
||||
throw new Error("Missing GitHub URL in MCP download response")
|
||||
}
|
||||
if (!mcpDetails.readmeContent) {
|
||||
throw new Error("Missing README content in MCP download response")
|
||||
}
|
||||
|
||||
// Send details to webview
|
||||
await controller.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
mcpDownloadDetails: mcpDetails,
|
||||
})
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
const { chatSettings } = await controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// Initialize task and show chat view
|
||||
await controller.initTask(task)
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
// Return an empty response - the client only cares if the call succeeded
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Failed to download MCP:", error)
|
||||
let errorMessage = "Failed to download MCP"
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = "MCP server not found in marketplace."
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Internal server error. Please try again later."
|
||||
} else if (!error.response && error.request) {
|
||||
errorMessage = "Network error. Please check your internet connection."
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Show error in both notification and marketplace UI
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await controller.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
error: errorMessage,
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
@@ -13,8 +11,6 @@ import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Restarts an MCP server connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the server name
|
||||
* @returns The updated list of MCP servers
|
||||
*/
|
||||
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
|
||||
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Command slash command logic
|
||||
*/
|
||||
export async function condense(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create slash service registry
|
||||
const slashService = createServiceRegistry("slash")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type SlashMethodHandler = ServiceMethodHandler
|
||||
export type SlashStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = slashService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleSlashServiceRequest = slashService.handleRequest
|
||||
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
|
||||
export const isStreamingMethod = slashService.isStreamingMethod
|
||||
|
||||
// Register all slash methods
|
||||
registerAllMethods()
|
||||
@@ -1,14 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { condense } from "./condense"
|
||||
import { reportBug } from "./reportBug"
|
||||
|
||||
// Register all slash service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("condense", condense)
|
||||
registerMethod("reportBug", reportBug)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Report bug slash command logic
|
||||
*/
|
||||
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { getLatestState } from "./getLatestState"
|
||||
import { resetState } from "./resetState"
|
||||
import { subscribeToState } from "./subscribeToState"
|
||||
import { toggleFavoriteModel } from "./toggleFavoriteModel"
|
||||
|
||||
// Streaming methods for this service
|
||||
export const streamingMethods = ["subscribeToState"]
|
||||
@@ -15,7 +13,5 @@ export const streamingMethods = ["subscribeToState"]
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLatestState", getLatestState)
|
||||
registerMethod("resetState", resetState)
|
||||
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
|
||||
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, EmptyRequest } from "../../../shared/proto/common"
|
||||
import { resetExtensionState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
* @param controller The controller instance
|
||||
* @param request An empty request (no parameters needed)
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(controller.context)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.abortTask()
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { updateGlobalState } from "@/core/storage/state"
|
||||
|
||||
/**
|
||||
* Toggles a model's favorite status
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the model ID to toggle
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function toggleFavoriteModel(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (!request.value) {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const { apiConfiguration } = await controller.getStateToPostToWebview()
|
||||
|
||||
if (!apiConfiguration) {
|
||||
throw new Error("API configuration not found")
|
||||
}
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(modelId)
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
telemetryService.captureModelFavoritesUsage(modelId, isFavorited)
|
||||
|
||||
// Post state to webview without changing any other configuration
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle favorite status for model ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { AskResponseRequest } from "../../../shared/proto/task"
|
||||
import { ClineAskResponse } from "../../../shared/WebviewMessage"
|
||||
|
||||
/**
|
||||
* Handles a response from the webview for a previous ask operation
|
||||
*
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing response type, optional text and optional images
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function askResponse(controller: Controller, request: AskResponseRequest): Promise<Empty> {
|
||||
try {
|
||||
if (!controller.task) {
|
||||
console.warn("askResponse: No active task to receive response")
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
// Map the string responseType to the ClineAskResponse enum
|
||||
let responseType: ClineAskResponse
|
||||
switch (request.responseType) {
|
||||
case "yesButtonClicked":
|
||||
responseType = "yesButtonClicked"
|
||||
break
|
||||
case "noButtonClicked":
|
||||
responseType = "noButtonClicked"
|
||||
break
|
||||
case "messageResponse":
|
||||
responseType = "messageResponse"
|
||||
break
|
||||
default:
|
||||
console.warn(`askResponse: Unknown response type: ${request.responseType}`)
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
// Call the task's handler for webview responses
|
||||
await controller.task.handleWebviewAskResponse(responseType, request.text, request.images)
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in askResponse handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Controller } from ".."
|
||||
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
|
||||
import { getGlobalState } from "../../storage/state"
|
||||
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
|
||||
|
||||
/**
|
||||
* Gets filtered task history
|
||||
@@ -11,49 +10,22 @@ import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
|
||||
*/
|
||||
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
|
||||
try {
|
||||
const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request
|
||||
const { favoritesOnly, searchQuery, sortBy } = request
|
||||
|
||||
// Get task history from global state
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const workspacePath = getWorkspacePath()
|
||||
|
||||
// Apply filters
|
||||
let filteredTasks = taskHistory.filter((item) => {
|
||||
// Basic filter: must have timestamp and task content
|
||||
const hasRequiredFields = item.ts && item.task
|
||||
if (!hasRequiredFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && !item.isFavorited) {
|
||||
return false
|
||||
if (favoritesOnly && hasRequiredFields) {
|
||||
return item.isFavorited === true
|
||||
}
|
||||
|
||||
// Apply current workspace filter if requested
|
||||
if (currentWorkspaceOnly) {
|
||||
let isInWorkspace = false
|
||||
|
||||
// First check the cwdOnTaskInitialization property - Only present on tasks from this change forward
|
||||
if (item.cwdOnTaskInitialization) {
|
||||
if (arePathsEqual(item.cwdOnTaskInitialization, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
// For tasks without cwdOnTaskInitialization, check the older shadowGitConfigWorkTree property
|
||||
if (!isInWorkspace && item.shadowGitConfigWorkTree) {
|
||||
if (arePathsEqual(item.shadowGitConfigWorkTree, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInWorkspace) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return hasRequiredFields
|
||||
})
|
||||
|
||||
// Apply search if provided
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { askResponse } from "./askResponse"
|
||||
import { cancelTask } from "./cancelTask"
|
||||
import { clearTask } from "./clearTask"
|
||||
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
|
||||
@@ -12,14 +11,11 @@ import { exportTaskWithId } from "./exportTaskWithId"
|
||||
import { getTaskHistory } from "./getTaskHistory"
|
||||
import { newTask } from "./newTask"
|
||||
import { showTaskWithId } from "./showTaskWithId"
|
||||
import { taskCompletionViewChanges } from "./taskCompletionViewChanges"
|
||||
import { taskFeedback } from "./taskFeedback"
|
||||
import { toggleTaskFavorite } from "./toggleTaskFavorite"
|
||||
|
||||
// Register all task service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("askResponse", askResponse)
|
||||
registerMethod("cancelTask", cancelTask)
|
||||
registerMethod("clearTask", clearTask)
|
||||
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
|
||||
@@ -28,7 +24,5 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getTaskHistory", getTaskHistory)
|
||||
registerMethod("newTask", newTask)
|
||||
registerMethod("showTaskWithId", showTaskWithId)
|
||||
registerMethod("taskCompletionViewChanges", taskCompletionViewChanges)
|
||||
registerMethod("taskFeedback", taskFeedback)
|
||||
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { Int64Request } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Shows task completion changes in a diff view
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the timestamp of the message
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
try {
|
||||
if (request.value && controller.task) {
|
||||
await controller.task.presentMultifileDiff(request.value, true)
|
||||
}
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in taskCompletionViewChanges handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
|
||||
/**
|
||||
* Handles task feedback submission (thumbs up/down)
|
||||
* @param controller The controller instance
|
||||
* @param request The StringRequest containing the feedback type ("thumbs_up" or "thumbs_down") in the value field
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskFeedback(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
if (!request.value) {
|
||||
console.warn("taskFeedback: Missing feedback type value")
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
try {
|
||||
if (controller.task?.taskId) {
|
||||
telemetryService.captureTaskFeedback(controller.task.taskId, request.value as any)
|
||||
} else {
|
||||
console.warn("taskFeedback: No active task to receive feedback")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in taskFeedback handler:", error)
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest } from "../../../shared/proto/common"
|
||||
import { OpenGraphData } from "../../../shared/proto/web"
|
||||
import { fetchOpenGraphData as fetchOGData } from "../../../integrations/misc/link-preview"
|
||||
import { convertDomainOpenGraphDataToProto } from "../../../shared/proto-conversions/web/open-graph-conversion"
|
||||
|
||||
/**
|
||||
* Fetches Open Graph metadata from a URL
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the URL to fetch metadata from
|
||||
* @returns Promise resolving to OpenGraphData
|
||||
*/
|
||||
export async function fetchOpenGraphData(controller: Controller, request: StringRequest): Promise<OpenGraphData> {
|
||||
try {
|
||||
const url = request.value || ""
|
||||
// Fetch open graph data using the existing utility
|
||||
const ogData = await fetchOGData(url)
|
||||
|
||||
// Convert domain model to proto model
|
||||
return convertDomainOpenGraphDataToProto(ogData)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data: ${request.value}`, error)
|
||||
// Return empty OpenGraphData object
|
||||
return OpenGraphData.create({})
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { checkIsImageUrl } from "./checkIsImageUrl"
|
||||
import { fetchOpenGraphData } from "./fetchOpenGraphData"
|
||||
|
||||
// Register all web service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("checkIsImageUrl", checkIsImageUrl)
|
||||
registerMethod("fetchOpenGraphData", fetchOpenGraphData)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import fs from "fs/promises"
|
||||
|
||||
/**
|
||||
* Processes text for slash commands and transforms them with appropriate instructions
|
||||
* This is called after parseMentions() to process any slash commands in the user's message
|
||||
*/
|
||||
export async function parseSlashCommands(
|
||||
text: string,
|
||||
workflowToggles: ClineRulesToggles,
|
||||
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
||||
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
|
||||
const commandReplacements: Record<string, string> = {
|
||||
newtask: newTaskToolResponse(),
|
||||
@@ -22,10 +17,10 @@ export async function parseSlashCommands(
|
||||
|
||||
// this currently allows matching prepended whitespace prior to /slash-command
|
||||
const tagPatterns = [
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
]
|
||||
|
||||
// if we find a valid match, we will return inside that block
|
||||
@@ -39,8 +34,7 @@ export async function parseSlashCommands(
|
||||
|
||||
const commandName = match[2] // casing matters
|
||||
|
||||
// we give preference to the default commands if the user has a file with the same name
|
||||
if (SUPPORTED_DEFAULT_COMMANDS.includes(commandName)) {
|
||||
if (SUPPORTED_COMMANDS.includes(commandName)) {
|
||||
const fullMatchStartIndex = match.index
|
||||
|
||||
// find position of slash command within the full match
|
||||
@@ -57,48 +51,6 @@ export async function parseSlashCommands(
|
||||
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||
}
|
||||
|
||||
// in practice we want to minimize this work, so we only do it if theres a possible match
|
||||
const enabledWorkflows = Object.entries(workflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => {
|
||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||
|
||||
return {
|
||||
fullPath: filePath,
|
||||
fileName: fileName,
|
||||
}
|
||||
})
|
||||
|
||||
// Then check if the command matches any enabled workflow filename
|
||||
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
||||
|
||||
if (matchingWorkflow) {
|
||||
try {
|
||||
// Read workflow file content from the full path
|
||||
const workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
|
||||
|
||||
// find position of slash command within the full match
|
||||
const fullMatchStartIndex = match.index
|
||||
const fullMatch = match[0]
|
||||
const relativeStartIndex = fullMatch.indexOf(match[1])
|
||||
|
||||
// calculate absolute indices in the original string
|
||||
const slashCommandStartIndex = fullMatchStartIndex + relativeStartIndex
|
||||
const slashCommandEndIndex = slashCommandStartIndex + match[1].length
|
||||
|
||||
// remove the slash command and add custom instructions at the top of this message
|
||||
const textWithoutSlashCommand =
|
||||
text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
|
||||
const processedText =
|
||||
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
|
||||
textWithoutSlashCommand
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
} catch (error) {
|
||||
console.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ export const GlobalFileNames = {
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
cursorRulesDir: ".cursor/rules",
|
||||
cursorRulesFile: ".cursorrules",
|
||||
windsurfRules: ".windsurfrules",
|
||||
|
||||
@@ -11,7 +11,6 @@ export type SecretKey =
|
||||
| "deepSeekApiKey"
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "fireworksApiKey"
|
||||
| "qwenApiKey"
|
||||
| "doubaoApiKey"
|
||||
| "mistralApiKey"
|
||||
@@ -70,9 +69,6 @@ export type GlobalStateKey =
|
||||
| "liteLlmModelId"
|
||||
| "liteLlmModelInfo"
|
||||
| "liteLlmUsePromptCache"
|
||||
| "fireworksModelId"
|
||||
| "fireworksModelMaxCompletionTokens"
|
||||
| "fireworksModelMaxTokens"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "requestyModelInfo"
|
||||
@@ -88,6 +84,5 @@ export type GlobalStateKey =
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
| "isNewUser"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -76,7 +76,6 @@ async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: bool
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
@@ -133,10 +132,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
@@ -163,7 +158,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -220,10 +214,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
|
||||
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
|
||||
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
|
||||
@@ -345,10 +335,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
@@ -356,7 +342,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
@@ -530,7 +515,6 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
"fireworksApiKey",
|
||||
"asksageApiKey",
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
|
||||
+8
-66
@@ -60,7 +60,7 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
|
||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
|
||||
import { constructNewFileContent } from "@core/assistant-message/diff"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "@core/mentions"
|
||||
@@ -80,7 +80,6 @@ import {
|
||||
ensureTaskDirectoryExists,
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
GlobalFileNames,
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
} from "@core/storage/disk"
|
||||
@@ -88,14 +87,13 @@ import {
|
||||
getGlobalClineRules,
|
||||
getLocalClineRules,
|
||||
refreshClineRulesToggles,
|
||||
ensureLocalClinerulesDirExists,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import {
|
||||
refreshExternalRulesToggles,
|
||||
getLocalWindsurfRules,
|
||||
getLocalCursorRules,
|
||||
} from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { parseSlashCommands } from "@core/slash-commands"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
@@ -232,37 +230,6 @@ export class Task {
|
||||
let effectiveApiConfiguration: ApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
taskId: this.taskId,
|
||||
onRetryAttempt: (attempt: number, maxRetries: number, delay: number, error: any) => {
|
||||
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
||||
if (lastApiReqStartedIndex !== -1) {
|
||||
try {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(
|
||||
this.clineMessages[lastApiReqStartedIndex].text || "{}",
|
||||
)
|
||||
currentApiReqInfo.retryStatus = {
|
||||
attempt: attempt, // attempt is already 1-indexed from retry.ts
|
||||
maxAttempts: maxRetries, // total attempts
|
||||
delaySec: Math.round(delay / 1000),
|
||||
errorSnippet: error?.message ? `${String(error.message).substring(0, 50)}...` : undefined,
|
||||
}
|
||||
// Clear previous cancelReason and streamingFailedMessage if we are retrying
|
||||
delete currentApiReqInfo.cancelReason
|
||||
delete currentApiReqInfo.streamingFailedMessage
|
||||
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify(currentApiReqInfo)
|
||||
|
||||
// Post the updated state to the webview so the UI reflects the retry attempt
|
||||
this.postStateToWebview().catch((e) =>
|
||||
console.error("Error posting state to webview in onRetryAttempt:", e),
|
||||
)
|
||||
|
||||
console.log(
|
||||
`[Task ${this.taskId}] API Auto-Retry Status Update: Attempt ${attempt}/${maxRetries}, Delay: ${delay}ms`,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`[Task ${this.taskId}] Error updating api_req_started with retryStatus:`, e)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
|
||||
@@ -358,7 +325,6 @@ export class Task {
|
||||
totalCost: apiMetrics.totalCost,
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
})
|
||||
@@ -631,9 +597,7 @@ export class Task {
|
||||
}
|
||||
|
||||
async doesLatestTaskCompletionHaveNewChanges() {
|
||||
if (!this.enableCheckpoints) {
|
||||
return false
|
||||
}
|
||||
if (!this.enableCheckpoints) return false
|
||||
|
||||
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
|
||||
const message = this.clineMessages[messageIndex]
|
||||
@@ -1687,20 +1651,6 @@ export class Task {
|
||||
|
||||
const errorMessage = this.formatErrorWithStatusCode(error)
|
||||
|
||||
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
|
||||
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
||||
if (lastApiReqStartedIndex !== -1) {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqStartedIndex].text || "{}")
|
||||
delete currentApiReqInfo.retryStatus
|
||||
|
||||
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
} satisfies ClineApiReqInfo)
|
||||
// this.ask will trigger postStateToWebview, so this change should be picked up.
|
||||
}
|
||||
|
||||
const { response } = await this.ask("api_req_failed", errorMessage)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
@@ -3358,7 +3308,7 @@ export class Task {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The user did not submit the bug, and provided feedback on the Github issue generated instead:\n<feedback>\n${text}\n</feedback>`,
|
||||
`The user provided feedback on the Github issue generated:\n<feedback>\n${text}\n</feedback>`,
|
||||
images,
|
||||
),
|
||||
)
|
||||
@@ -3844,11 +3794,8 @@ export class Task {
|
||||
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
|
||||
// (it's worth removing a few months from now)
|
||||
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}")
|
||||
delete currentApiReqInfo.retryStatus // Clear retry status when request is finalized
|
||||
|
||||
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWrites: cacheWriteTokens,
|
||||
@@ -3963,7 +3910,7 @@ export class Task {
|
||||
assistantMessage += chunk.text
|
||||
// parse raw assistant message into content blocks
|
||||
const prevLength = this.assistantMessageContent.length
|
||||
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
|
||||
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
|
||||
if (this.assistantMessageContent.length > prevLength) {
|
||||
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
|
||||
}
|
||||
@@ -4117,8 +4064,6 @@ export class Task {
|
||||
// Track if we need to check clinerulesFile
|
||||
let needsClinerulesFileCheck = false
|
||||
|
||||
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
|
||||
|
||||
const processUserContent = async () => {
|
||||
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
|
||||
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
|
||||
@@ -4141,10 +4086,7 @@ export class Task {
|
||||
)
|
||||
|
||||
// when parsing slash commands, we still want to allow the user to provide their desired context
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||
parsedText,
|
||||
workflowToggles,
|
||||
)
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
|
||||
|
||||
if (needsCheck) {
|
||||
needsClinerulesFileCheck = true
|
||||
@@ -4170,7 +4112,7 @@ export class Task {
|
||||
// After processing content, check clinerulesData if needed
|
||||
let clinerulesError = false
|
||||
if (needsClinerulesFileCheck) {
|
||||
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
|
||||
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
|
||||
}
|
||||
|
||||
// Return all results
|
||||
|
||||
@@ -149,28 +149,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
this.controller.clearTask()
|
||||
|
||||
this.outputChannel.appendLine("Webview view resolved")
|
||||
|
||||
// Set the title to include the keyboard shortcut
|
||||
if (this.view && "title" in this.view) {
|
||||
// Check if the view object has a title property
|
||||
const isMac = process.platform === "darwin"
|
||||
const shortcutDisplay = isMac ? " (⌘+')" : " (Ctrl+')"
|
||||
const baseTitle = this.context.extension.packageJSON.displayName || "Cline"
|
||||
const newTitleWithShortcut = `${baseTitle}${shortcutDisplay}`
|
||||
|
||||
const currentTitle = this.view.title
|
||||
|
||||
if (typeof currentTitle === "string") {
|
||||
if (!currentTitle.includes(shortcutDisplay)) {
|
||||
// Title exists and is a string, but doesn't have the shortcut
|
||||
this.view.title = newTitleWithShortcut
|
||||
}
|
||||
// If it includes shortcutDisplay, do nothing
|
||||
} else {
|
||||
// Title is undefined or not a string (e.g., for sidebar initially)
|
||||
this.view.title = newTitleWithShortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +328,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="http://localhost:8097"></script>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
|
||||
+31
-194
@@ -28,7 +28,7 @@ let outputChannel: vscode.OutputChannel
|
||||
|
||||
// This method is called when your extension is activated
|
||||
// Your extension is activated the very first time the command is executed
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
@@ -36,9 +36,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
Logger.initialize(outputChannel)
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
// Version checking for autoupdate notification
|
||||
const currentVersion = context.extension.packageJSON.version
|
||||
const previousVersion = context.globalState.get<string>("clineVersion")
|
||||
const sidebarWebview = new WebviewProvider(context, outputChannel)
|
||||
|
||||
// Initialize test mode and add disposables to context
|
||||
@@ -52,29 +49,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
// Perform post-update actions if necessary
|
||||
try {
|
||||
if (!previousVersion || currentVersion !== previousVersion) {
|
||||
Logger.log(`Cline version changed: ${previousVersion} -> ${currentVersion}. First run or update detected.`)
|
||||
const lastShownPopupNotificationVersion = context.globalState.get<string>("clineLastPopupNotificationVersion")
|
||||
|
||||
if (currentVersion !== lastShownPopupNotificationVersion && previousVersion) {
|
||||
// Show VS Code popup notification as this version hasn't been notified yet without doing it for fresh installs
|
||||
const message = `Cline has been updated to v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
vscode.window.showInformationMessage(message)
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
// Always update the main version tracker for the next launch.
|
||||
await context.globalState.update("clineVersion", currentVersion)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
|
||||
const openChat = async (instance?: WebviewProvider) => {
|
||||
@@ -283,8 +257,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.addToChat", async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
|
||||
await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
|
||||
await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
return
|
||||
@@ -364,100 +336,50 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
const CONTEXT_LINES_TO_EXPAND = 3
|
||||
const START_OF_LINE_CHAR_INDEX = 0
|
||||
const LINE_COUNT_ADJUSTMENT_FOR_ZERO_INDEXING = 1
|
||||
|
||||
// Register code action provider
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider(
|
||||
"*",
|
||||
new (class implements vscode.CodeActionProvider {
|
||||
public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.Refactor]
|
||||
public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]
|
||||
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range,
|
||||
context: vscode.CodeActionContext,
|
||||
): vscode.CodeAction[] {
|
||||
const actions: vscode.CodeAction[] = []
|
||||
const editor = vscode.window.activeTextEditor // Get active editor for selection check
|
||||
// Expand range to include surrounding 3 lines
|
||||
const expandedRange = new vscode.Range(
|
||||
Math.max(0, range.start.line - 3),
|
||||
0,
|
||||
Math.min(document.lineCount - 1, range.end.line + 3),
|
||||
document.lineAt(Math.min(document.lineCount - 1, range.end.line + 3)).text.length,
|
||||
)
|
||||
|
||||
// Expand range to include surrounding 3 lines or use selection if broader
|
||||
const selection = editor?.selection
|
||||
let expandedRange = range
|
||||
if (
|
||||
editor &&
|
||||
selection &&
|
||||
!selection.isEmpty &&
|
||||
selection.contains(range.start) &&
|
||||
selection.contains(range.end)
|
||||
) {
|
||||
expandedRange = selection
|
||||
} else {
|
||||
expandedRange = new vscode.Range(
|
||||
Math.max(0, range.start.line - CONTEXT_LINES_TO_EXPAND),
|
||||
START_OF_LINE_CHAR_INDEX,
|
||||
Math.min(
|
||||
document.lineCount - LINE_COUNT_ADJUSTMENT_FOR_ZERO_INDEXING,
|
||||
range.end.line + CONTEXT_LINES_TO_EXPAND,
|
||||
),
|
||||
document.lineAt(
|
||||
Math.min(
|
||||
document.lineCount - LINE_COUNT_ADJUSTMENT_FOR_ZERO_INDEXING,
|
||||
range.end.line + CONTEXT_LINES_TO_EXPAND,
|
||||
),
|
||||
).text.length,
|
||||
)
|
||||
}
|
||||
|
||||
// Add to Cline (Always available)
|
||||
const addAction = new vscode.CodeAction("Add to Cline", vscode.CodeActionKind.QuickFix)
|
||||
addAction.command = {
|
||||
command: "cline.addToChat",
|
||||
title: "Add to Cline",
|
||||
arguments: [expandedRange, context.diagnostics],
|
||||
}
|
||||
actions.push(addAction)
|
||||
|
||||
// Explain with Cline (Always available)
|
||||
const explainAction = new vscode.CodeAction("Explain with Cline", vscode.CodeActionKind.RefactorExtract) // Using a refactor kind
|
||||
explainAction.command = {
|
||||
command: "cline.explainCode",
|
||||
title: "Explain with Cline",
|
||||
arguments: [expandedRange],
|
||||
const fixAction = new vscode.CodeAction("Fix with Cline", vscode.CodeActionKind.QuickFix)
|
||||
fixAction.command = {
|
||||
command: "cline.fixWithCline",
|
||||
title: "Fix with Cline",
|
||||
arguments: [expandedRange, context.diagnostics],
|
||||
}
|
||||
actions.push(explainAction)
|
||||
|
||||
// Improve with Cline (Always available)
|
||||
const improveAction = new vscode.CodeAction("Improve with Cline", vscode.CodeActionKind.RefactorRewrite) // Using a refactor kind
|
||||
improveAction.command = {
|
||||
command: "cline.improveCode",
|
||||
title: "Improve with Cline",
|
||||
arguments: [expandedRange],
|
||||
}
|
||||
actions.push(improveAction)
|
||||
|
||||
// Fix with Cline (Only if diagnostics exist)
|
||||
// Only show actions when there are errors
|
||||
if (context.diagnostics.length > 0) {
|
||||
const fixAction = new vscode.CodeAction("Fix with Cline", vscode.CodeActionKind.QuickFix)
|
||||
fixAction.isPreferred = true
|
||||
fixAction.command = {
|
||||
command: "cline.fixWithCline",
|
||||
title: "Fix with Cline",
|
||||
arguments: [expandedRange, context.diagnostics],
|
||||
}
|
||||
actions.push(fixAction)
|
||||
return [addAction, fixAction]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return actions
|
||||
}
|
||||
})(),
|
||||
{
|
||||
providedCodeActionKinds: [
|
||||
vscode.CodeActionKind.QuickFix,
|
||||
vscode.CodeActionKind.RefactorExtract,
|
||||
vscode.CodeActionKind.RefactorRewrite,
|
||||
],
|
||||
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix],
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -484,106 +406,21 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.explainCode", async (range: vscode.Range) => {
|
||||
await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
|
||||
await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to explain.")
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
|
||||
const prompt = `Explain the following code from ${fileMention}:\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
|
||||
await visibleWebview?.controller.initTask(prompt)
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.improveCode", async (range: vscode.Range) => {
|
||||
await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
|
||||
await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to improve.")
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
|
||||
const prompt = `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
|
||||
await visibleWebview?.controller.initTask(prompt)
|
||||
}),
|
||||
)
|
||||
|
||||
// Register the focusChatInput command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.focusChatInput", async () => {
|
||||
let activeWebviewProvider: WebviewProvider | undefined = WebviewProvider.getVisibleInstance()
|
||||
|
||||
// If a tab is visible and active, ensure it's fully revealed (might be redundant but safe)
|
||||
if (activeWebviewProvider?.view && activeWebviewProvider.view.hasOwnProperty("reveal")) {
|
||||
const panelView = activeWebviewProvider.view as vscode.WebviewPanel
|
||||
panelView.reveal(panelView.viewColumn)
|
||||
} else if (!activeWebviewProvider) {
|
||||
// No webview is currently visible, try to activate the sidebar
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200)) // Allow time for focus
|
||||
activeWebviewProvider = WebviewProvider.getSidebarInstance()
|
||||
|
||||
if (!activeWebviewProvider) {
|
||||
// Sidebar didn't become active (might be closed or not in current view container)
|
||||
// Check for existing tab panels
|
||||
const tabInstances = WebviewProvider.getTabInstances()
|
||||
if (tabInstances.length > 0) {
|
||||
const potentialTabInstance = tabInstances[tabInstances.length - 1] // Get the most recent one
|
||||
if (potentialTabInstance.view && potentialTabInstance.view.hasOwnProperty("reveal")) {
|
||||
const panelView = potentialTabInstance.view as vscode.WebviewPanel
|
||||
panelView.reveal(panelView.viewColumn)
|
||||
activeWebviewProvider = potentialTabInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeWebviewProvider) {
|
||||
// No existing Cline view found at all, open a new tab
|
||||
await vscode.commands.executeCommand("cline.openInNewTab")
|
||||
// After openInNewTab, a new webview is created. We need to get this new instance.
|
||||
// It might take a moment for it to register.
|
||||
await pWaitFor(
|
||||
() => {
|
||||
const visibleInstance = WebviewProvider.getVisibleInstance()
|
||||
// Ensure a boolean is returned
|
||||
return !!(visibleInstance?.view && visibleInstance.view.hasOwnProperty("reveal"))
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
)
|
||||
activeWebviewProvider = WebviewProvider.getVisibleInstance()
|
||||
}
|
||||
}
|
||||
// At this point, activeWebviewProvider should be the one we want to send the message to.
|
||||
// It could still be undefined if opening a new tab failed or timed out.
|
||||
if (activeWebviewProvider) {
|
||||
activeWebviewProvider.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "focusChatInput",
|
||||
})
|
||||
} else {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
vscode.window.showErrorMessage(
|
||||
"Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
)
|
||||
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",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -91,9 +91,9 @@ export class DiffViewProvider {
|
||||
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
|
||||
|
||||
// If the first visible line moved upward, user scrolled up
|
||||
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
// this.shouldAutoScroll = false
|
||||
// }
|
||||
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
this.shouldAutoScroll = false
|
||||
}
|
||||
|
||||
// Always update our tracking variable
|
||||
this.lastFirstVisibleLine = currentFirstVisibleLine
|
||||
@@ -433,6 +433,12 @@ export class DiffViewProvider {
|
||||
|
||||
// close editor if open?
|
||||
async reset() {
|
||||
// releasing memory by clearing the diff editor
|
||||
try {
|
||||
await this.closeAllDiffViews()
|
||||
} catch (error) {
|
||||
console.error("Error closing diff views:", error)
|
||||
}
|
||||
this.editType = undefined
|
||||
this.isEditing = false
|
||||
this.originalContent = undefined
|
||||
|
||||
@@ -108,47 +108,6 @@ export class TerminalManager {
|
||||
if (disposable) {
|
||||
this.disposables.push(disposable)
|
||||
}
|
||||
|
||||
// Add a listener for terminal state changes to detect CWD updates
|
||||
try {
|
||||
const stateChangeDisposable = vscode.window.onDidChangeTerminalState((terminal) => {
|
||||
const terminalInfo = this.findTerminalInfoByTerminal(terminal)
|
||||
if (terminalInfo && terminalInfo.pendingCwdChange && terminalInfo.cwdResolved) {
|
||||
// Check if CWD has been updated to match the expected path
|
||||
if (this.isCwdMatchingExpected(terminalInfo)) {
|
||||
const resolver = terminalInfo.cwdResolved.resolve
|
||||
terminalInfo.pendingCwdChange = undefined
|
||||
terminalInfo.cwdResolved = undefined
|
||||
resolver()
|
||||
}
|
||||
}
|
||||
})
|
||||
this.disposables.push(stateChangeDisposable)
|
||||
} catch (error) {
|
||||
console.error("Error setting up onDidChangeTerminalState", error)
|
||||
}
|
||||
}
|
||||
|
||||
//Find a TerminalInfo by its VSCode Terminal instance
|
||||
private findTerminalInfoByTerminal(terminal: vscode.Terminal): TerminalInfo | undefined {
|
||||
const terminals = TerminalRegistry.getAllTerminals()
|
||||
return terminals.find((t) => t.terminal === terminal)
|
||||
}
|
||||
|
||||
//Check if a terminal's CWD matches its expected pending change
|
||||
private isCwdMatchingExpected(terminalInfo: TerminalInfo): boolean {
|
||||
if (!terminalInfo.pendingCwdChange) {
|
||||
return false
|
||||
}
|
||||
|
||||
const currentCwd = terminalInfo.terminal.shellIntegration?.cwd?.fsPath
|
||||
const targetCwd = vscode.Uri.file(terminalInfo.pendingCwdChange).fsPath
|
||||
|
||||
if (!currentCwd) {
|
||||
return false
|
||||
}
|
||||
|
||||
return arePathsEqual(currentCwd, targetCwd)
|
||||
}
|
||||
|
||||
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
|
||||
@@ -237,37 +196,8 @@ export class TerminalManager {
|
||||
// If no matching terminal exists, try to find any non-busy terminal
|
||||
const availableTerminal = terminals.find((t) => !t.busy)
|
||||
if (availableTerminal) {
|
||||
// Set up promise and tracking for CWD change
|
||||
const cwdPromise = new Promise<void>((resolve, reject) => {
|
||||
availableTerminal.pendingCwdChange = cwd
|
||||
availableTerminal.cwdResolved = { resolve, reject }
|
||||
})
|
||||
|
||||
// Navigate back to the desired directory
|
||||
await this.runCommand(availableTerminal, `cd "${cwd}"`)
|
||||
|
||||
// Either resolve immediately if CWD already updated or wait for event/timeout
|
||||
if (this.isCwdMatchingExpected(availableTerminal)) {
|
||||
if (availableTerminal.cwdResolved) {
|
||||
availableTerminal.cwdResolved.resolve()
|
||||
}
|
||||
availableTerminal.pendingCwdChange = undefined
|
||||
availableTerminal.cwdResolved = undefined
|
||||
} else {
|
||||
try {
|
||||
// Wait with a timeout for state change event to resolve
|
||||
await Promise.race([
|
||||
cwdPromise,
|
||||
new Promise<void>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
|
||||
),
|
||||
])
|
||||
} catch (err) {
|
||||
// Clear pending state on timeout
|
||||
availableTerminal.pendingCwdChange = undefined
|
||||
availableTerminal.cwdResolved = undefined
|
||||
}
|
||||
}
|
||||
this.terminalIds.add(availableTerminal.id)
|
||||
return availableTerminal
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ export interface TerminalInfo {
|
||||
busy: boolean
|
||||
lastCommand: string
|
||||
id: number
|
||||
pendingCwdChange?: string
|
||||
cwdResolved?: {
|
||||
resolve: () => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
}
|
||||
|
||||
// Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output).
|
||||
|
||||
@@ -16,7 +16,6 @@ import { discoverChromeInstances, testBrowserConnection, isPortOpen } from "./Br
|
||||
import * as chromeLauncher from "chrome-launcher"
|
||||
import { Controller } from "@core/controller"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import os from "os"
|
||||
|
||||
interface PCRStats {
|
||||
puppeteer: { launch: typeof launch }
|
||||
@@ -124,20 +123,45 @@ export class BrowserSession {
|
||||
}
|
||||
|
||||
async relaunchChromeDebugMode(controller: Controller) {
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
"This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
|
||||
{ modal: true },
|
||||
"Yes",
|
||||
)
|
||||
|
||||
if (result !== "Yes") {
|
||||
controller?.postMessageToWebview({
|
||||
type: "browserRelaunchResult",
|
||||
success: false,
|
||||
text: "Operation cancelled by user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const userDataDir = path.join(os.tmpdir(), "chrome-debug-profile")
|
||||
// Chrome-launcher's killAll only kills instances it launched
|
||||
// We need to handle system Chrome processes separately
|
||||
await this.killAllChromeBrowsers()
|
||||
|
||||
// Wait a moment for Chrome to fully shut down
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// Instead of using any default flags, use a minimal set to ensure session persistence
|
||||
// This closely mimics running "google-chrome-stable --remote-debugging-port=9222" from the CLI
|
||||
const chromeFlags = [
|
||||
"--remote-debugging-port=" + DEBUG_PORT,
|
||||
"--disable-notifications",
|
||||
// Do not add any flags that might interfere with profile data
|
||||
]
|
||||
|
||||
const installation = chromeLauncher.Launcher.getFirstInstallation()
|
||||
if (!installation) {
|
||||
throw new Error("Could not find Chrome installation on this system")
|
||||
}
|
||||
console.info("chrome installation", installation)
|
||||
|
||||
const args = [
|
||||
`--remote-debugging-port=${DEBUG_PORT}`,
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
"--disable-notifications",
|
||||
"chrome://newtab",
|
||||
]
|
||||
// Prepare the command arguments
|
||||
const args = [`--remote-debugging-port=${DEBUG_PORT}`, "--disable-notifications", "chrome://newtab"]
|
||||
|
||||
// Spawn Chrome as a detached process
|
||||
const chromeProcess = spawn(installation, args, {
|
||||
|
||||
@@ -103,7 +103,6 @@ export class McpHub {
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
@@ -181,120 +180,6 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServerRPC(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.clientVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
if (config.transportType === "sse") {
|
||||
transport = new SSEClientTransport(new URL(config.url), {})
|
||||
} else {
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...config.env,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
}
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
disabled: config.disabled,
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
if (config.transportType === "stdio") {
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = (transport as StdioClientTransport).stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
// Check if output contains INFO level log
|
||||
const isInfoLog = !/\berror\b/i.test(output)
|
||||
|
||||
if (isInfoLog) {
|
||||
// Log normal informational messages
|
||||
console.info(`Server "${name}" info:`, output)
|
||||
} else {
|
||||
// Treat as error log
|
||||
console.error(`Server "${name}" stderr:`, output)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
this.appendErrorMessage(connection, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
}
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
@@ -609,36 +494,6 @@ export class McpHub {
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnectionRPC(serverName: string): Promise<McpServer[]> {
|
||||
this.isConnecting = true
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const inMemoryConfig = connection?.server.config
|
||||
if (inMemoryConfig) {
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServerRPC(serverName, JSON.parse(inMemoryConfig))
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnecting = false
|
||||
|
||||
const config = await this.readAndValidateMcpSettingsFile()
|
||||
if (!config) {
|
||||
throw new Error("Failed to read or validate MCP settings")
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ class PostHogClientProvider {
|
||||
this.client = new PostHog(posthogConfig.apiKey, {
|
||||
host: posthogConfig.host,
|
||||
enableExceptionAutocapture: false,
|
||||
defaultOptIn: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ class PostHogClient {
|
||||
BROWSER_TOOL_END: "task.browser_tool_end",
|
||||
// Tracks when browser errors occur
|
||||
BROWSER_ERROR: "task.browser_error",
|
||||
// Tracks Gemini API specific performance metrics
|
||||
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
|
||||
// Collection of all task events
|
||||
TASK_COLLECTION: "task.collection",
|
||||
},
|
||||
@@ -732,43 +730,6 @@ class PostHogClient {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures Gemini API performance metrics.
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param modelId Specific Gemini model ID
|
||||
* @param data Performance data including TTFT, durations, token counts, cache stats, and API success status
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureGeminiApiPerformance(
|
||||
taskId: string,
|
||||
modelId: string,
|
||||
data: {
|
||||
ttftSec?: number
|
||||
totalDurationSec?: number
|
||||
promptTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheHit: boolean
|
||||
cacheHitPercentage?: number
|
||||
apiSuccess: boolean
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
taskId,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
},
|
||||
collect,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the user uses the model favorite button in the model picker
|
||||
* @param model The name of the model the user has interacted with
|
||||
|
||||
@@ -5,7 +5,6 @@ import { execa } from "execa"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
|
||||
import {
|
||||
getWorkspacePath,
|
||||
validateWorkspacePath,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
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"
|
||||
|
||||
@@ -515,8 +515,8 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
|
||||
const askText = message.partialMessage.text
|
||||
|
||||
// Automatically respond to different types of asks
|
||||
setTimeout(async () => {
|
||||
await autoRespondToAsk(webviewProvider, askType, askText)
|
||||
setTimeout(() => {
|
||||
autoRespondToAsk(webviewProvider, askType, askText)
|
||||
}, 100) // Small delay to ensure the message is processed first
|
||||
}
|
||||
|
||||
@@ -538,64 +538,65 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
|
||||
* @param askType The type of ask message
|
||||
* @param askText The text content of the ask message
|
||||
*/
|
||||
async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): Promise<void> {
|
||||
function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): void {
|
||||
if (!webviewProvider.controller) {
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log(`Auto-responding to ask type: ${askType}`)
|
||||
|
||||
// Default to approving most actions
|
||||
let responseType = "yesButtonClicked"
|
||||
let responseText: string | undefined
|
||||
let responseImages: string[] | undefined
|
||||
// Create a response message based on the ask type
|
||||
const response: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked", // Default to approving most actions
|
||||
}
|
||||
|
||||
// Handle specific ask types differently if needed
|
||||
switch (askType) {
|
||||
case "followup":
|
||||
// For follow-up questions, provide a generic response
|
||||
responseType = "messageResponse"
|
||||
responseText = "I can't answer any questions right now, use your best judgment."
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "I can't answer any questions right now, use your best judgment."
|
||||
break
|
||||
|
||||
case "api_req_failed":
|
||||
// Always retry API requests
|
||||
responseType = "yesButtonClicked" // "Retry" button
|
||||
response.askResponse = "yesButtonClicked" // "Retry" button
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
// Accept the completion
|
||||
responseType = "messageResponse"
|
||||
responseText = "Task completed successfully."
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Task completed successfully."
|
||||
break
|
||||
|
||||
case "mistake_limit_reached":
|
||||
// Provide guidance to continue
|
||||
responseType = "messageResponse"
|
||||
responseText = "Try breaking down the task into smaller steps."
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Try breaking down the task into smaller steps."
|
||||
break
|
||||
|
||||
case "auto_approval_max_req_reached":
|
||||
// Reset the count to continue
|
||||
responseType = "yesButtonClicked" // "Reset and continue" button
|
||||
response.askResponse = "yesButtonClicked" // "Reset and continue" button
|
||||
break
|
||||
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
// Resume the task
|
||||
responseType = "messageResponse"
|
||||
response.askResponse = "messageResponse"
|
||||
break
|
||||
|
||||
case "new_task":
|
||||
// Decline creating a new task to keep the current task running
|
||||
responseType = "messageResponse"
|
||||
responseText = "Continue with the current task."
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Continue with the current task."
|
||||
break
|
||||
|
||||
case "plan_mode_respond":
|
||||
// Respond to plan mode with a message to toggle to Act mode
|
||||
responseType = "messageResponse"
|
||||
responseText = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
|
||||
|
||||
// Automatically toggle to Act mode after responding
|
||||
setTimeout(async () => {
|
||||
@@ -615,16 +616,8 @@ async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: Cline
|
||||
}
|
||||
|
||||
// Send the response message
|
||||
try {
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType,
|
||||
text: responseText,
|
||||
images: responseImages,
|
||||
})
|
||||
Logger.log(`Auto-responded to ${askType} with ${responseType}`)
|
||||
} catch (error) {
|
||||
Logger.log(`Error sending askResponse: ${error}`)
|
||||
}
|
||||
webviewProvider.controller.handleWebviewMessage(response)
|
||||
Logger.log(`Auto-responded to ${askType} with ${response.askResponse}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,18 +22,18 @@ export interface AutoApprovalSettings {
|
||||
|
||||
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
|
||||
version: 1,
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
actions: {
|
||||
readFiles: true,
|
||||
readFiles: false,
|
||||
readFilesExternally: false,
|
||||
editFiles: false,
|
||||
editFilesExternally: false,
|
||||
executeSafeCommands: true,
|
||||
executeSafeCommands: false,
|
||||
executeAllCommands: false,
|
||||
useBrowser: false,
|
||||
useMcp: false,
|
||||
},
|
||||
maxRequests: 20,
|
||||
enableNotifications: false,
|
||||
favorites: ["enableAutoApprove", "readFiles", "editFiles"],
|
||||
favorites: [],
|
||||
}
|
||||
|
||||
@@ -115,7 +115,6 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
@@ -143,7 +142,6 @@ export interface ExtensionState {
|
||||
vscMachineId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
workflowToggles: ClineRulesToggles
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
}
|
||||
@@ -208,7 +206,6 @@ export type ClineSay =
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
| "load_mcp_documentation"
|
||||
| "info" // Added for general informational messages like retry status
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
@@ -277,14 +274,8 @@ export interface ClineApiReqInfo {
|
||||
cost?: number
|
||||
cancelReason?: ClineApiReqCancelReason
|
||||
streamingFailedMessage?: string
|
||||
retryStatus?: {
|
||||
attempt: number
|
||||
maxAttempts: number
|
||||
delaySec: number
|
||||
errorSnippet?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
|
||||
@@ -10,7 +10,6 @@ export type HistoryItem = {
|
||||
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
cwdOnTaskInitialization?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
||||
@@ -14,16 +14,21 @@ export interface WebviewMessage {
|
||||
| "newTask"
|
||||
| "condense"
|
||||
| "reportBug"
|
||||
| "askResponse"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "resetState"
|
||||
| "openInBrowser"
|
||||
| "openMention"
|
||||
| "showChatView"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserRelaunchResult"
|
||||
| "togglePlanActMode"
|
||||
| "taskCompletionViewChanges"
|
||||
| "openExtensionSettings"
|
||||
| "requestVsCodeLmModels"
|
||||
| "toggleToolAutoApprove"
|
||||
@@ -32,11 +37,13 @@ export interface WebviewMessage {
|
||||
| "authStateChanged"
|
||||
| "authCallback"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "telemetrySetting"
|
||||
| "openSettings"
|
||||
| "fetchOpenGraphData"
|
||||
| "invoke"
|
||||
| "updateSettings"
|
||||
| "clearAllTaskHistory"
|
||||
@@ -44,21 +51,24 @@ export interface WebviewMessage {
|
||||
| "optionsResponse"
|
||||
| "requestTotalTasksSize"
|
||||
| "relaunchChromeDebugMode"
|
||||
| "taskFeedback"
|
||||
| "scrollToSettings"
|
||||
| "searchFiles"
|
||||
| "toggleFavoriteModel"
|
||||
| "grpc_request"
|
||||
| "grpc_request_cancel"
|
||||
| "toggleClineRule"
|
||||
| "toggleCursorRule"
|
||||
| "toggleWindsurfRule"
|
||||
| "toggleWorkflow"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
| "updateTerminalConnectionTimeout"
|
||||
| "setActiveQuote"
|
||||
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
@@ -86,6 +96,8 @@ export interface WebviewMessage {
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
customInstructionsSetting?: string
|
||||
// For task feedback
|
||||
feedbackType?: TaskFeedbackType
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
// For toggleFavoriteModel
|
||||
@@ -100,10 +112,9 @@ export interface WebviewMessage {
|
||||
grpc_request_cancel?: {
|
||||
request_id: string // ID of the request to cancel
|
||||
}
|
||||
// For cline rules and workflows
|
||||
// For cline rules
|
||||
isGlobal?: boolean
|
||||
rulePath?: string
|
||||
workflowPath?: string
|
||||
enabled?: boolean
|
||||
filename?: string
|
||||
|
||||
|
||||
+8
-18
@@ -19,7 +19,6 @@ export type ApiProvider =
|
||||
| "vscode-lm"
|
||||
| "cline"
|
||||
| "litellm"
|
||||
| "fireworks"
|
||||
| "asksage"
|
||||
| "xai"
|
||||
| "sambanova"
|
||||
@@ -71,10 +70,6 @@ export interface ApiHandlerOptions {
|
||||
requestyModelInfo?: ModelInfo
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
fireworksApiKey?: string
|
||||
fireworksModelId?: string
|
||||
fireworksModelMaxCompletionTokens?: number
|
||||
fireworksModelMaxTokens?: number
|
||||
qwenApiKey?: string
|
||||
doubaoApiKey?: string
|
||||
mistralApiKey?: string
|
||||
@@ -88,7 +83,6 @@ export interface ApiHandlerOptions {
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
requestTimeoutMs?: number
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
@@ -115,7 +109,6 @@ export interface ModelInfo {
|
||||
outputPrice?: number // Output price per million tokens when budget > 0
|
||||
outputPriceTiers?: PriceTier[] // Optional: Tiered output price when budget > 0
|
||||
}
|
||||
supportsGlobalEndpoint?: boolean // Whether the model supports a global endpoint with Vertex AI
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
@@ -405,7 +398,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
cacheWritesPrice: 1.0,
|
||||
@@ -416,7 +408,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.075,
|
||||
outputPrice: 0.3,
|
||||
},
|
||||
@@ -425,7 +416,6 @@ export const vertexModels = {
|
||||
contextWindow: 32_767,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -434,7 +424,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -451,7 +440,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.31,
|
||||
@@ -475,7 +463,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
thinkingConfig: {
|
||||
@@ -488,7 +475,6 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -558,10 +544,6 @@ export const vertexModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const vertexGlobalModels: Record<string, ModelInfo> = Object.fromEntries(
|
||||
Object.entries(vertexModels).filter(([_k, v]) => v.hasOwnProperty("supportsGlobalEndpoint")),
|
||||
) as Record<string, ModelInfo>
|
||||
|
||||
export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
@@ -578,6 +560,14 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
|
||||
export type GeminiModelId = keyof typeof geminiModels
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
|
||||
export const geminiModels = {
|
||||
"gemini-2.5-pro-exp-03-25": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-preview-05-06": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
|
||||
@@ -2,24 +2,22 @@ import { RuleFileRequest } from "../../proto/file"
|
||||
|
||||
// Helper for creating delete requests
|
||||
export const DeleteRuleFileRequest = {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
rulePath: params.rulePath,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Helper for creating create requests
|
||||
export const CreateRuleFileRequest = {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
filename: params.filename,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { OpenGraphData as DomainOpenGraphData } from "@integrations/misc/link-preview"
|
||||
import { OpenGraphData as ProtoOpenGraphData } from "@shared/proto/web"
|
||||
|
||||
/**
|
||||
* Converts domain OpenGraphData objects to proto OpenGraphData objects
|
||||
* @param ogData Domain OpenGraphData object
|
||||
* @returns Proto OpenGraphData object
|
||||
*/
|
||||
export function convertDomainOpenGraphDataToProto(ogData: DomainOpenGraphData): ProtoOpenGraphData {
|
||||
return ProtoOpenGraphData.create({
|
||||
title: ogData.title || "",
|
||||
description: ogData.description || "",
|
||||
image: ogData.image || "",
|
||||
url: ogData.url || "",
|
||||
siteName: ogData.siteName || "",
|
||||
type: ogData.type || "",
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
|
||||
import { Empty, Metadata, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -73,8 +73,6 @@ export interface RuleFileRequest {
|
||||
rulePath?: string | undefined
|
||||
/** Filename field for createRuleFile (optional) */
|
||||
filename?: string | undefined
|
||||
/** Type of the file to create (optional) */
|
||||
type?: string | undefined
|
||||
}
|
||||
|
||||
/** Result for rule file operations with meaningful data only */
|
||||
@@ -684,7 +682,7 @@ export const GitCommit: MessageFns<GitCommit> = {
|
||||
}
|
||||
|
||||
function createBaseRuleFileRequest(): RuleFileRequest {
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined, type: undefined }
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
|
||||
}
|
||||
|
||||
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
@@ -701,9 +699,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
writer.uint32(34).string(message.filename)
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
writer.uint32(42).string(message.type)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -746,14 +741,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.filename = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.type = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -769,7 +756,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
isGlobal: isSet(object.isGlobal) ? globalThis.Boolean(object.isGlobal) : false,
|
||||
rulePath: isSet(object.rulePath) ? globalThis.String(object.rulePath) : undefined,
|
||||
filename: isSet(object.filename) ? globalThis.String(object.filename) : undefined,
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -787,9 +773,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
obj.filename = message.filename
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
obj.type = message.type
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -803,7 +786,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.isGlobal = object.isGlobal ?? false
|
||||
message.rulePath = object.rulePath ?? undefined
|
||||
message.filename = object.filename ?? undefined
|
||||
message.type = object.type ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -906,15 +888,6 @@ export const FileServiceDefinition = {
|
||||
name: "FileService",
|
||||
fullName: "cline.FileService",
|
||||
methods: {
|
||||
/** Copies text to clipboard */
|
||||
copyToClipboard: {
|
||||
name: "copyToClipboard",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Opens a file in the editor */
|
||||
openFile: {
|
||||
name: "openFile",
|
||||
@@ -960,15 +933,6 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Select images from the file system and return as data URLs */
|
||||
selectImages: {
|
||||
name: "selectImages",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: StringArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Convert URIs to workspace-relative paths */
|
||||
getRelativePaths: {
|
||||
name: "getRelativePaths",
|
||||
|
||||
+1
-17
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, Metadata, StringRequest } from "./common"
|
||||
import { Metadata } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -1004,22 +1004,6 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
downloadMcp: {
|
||||
name: "downloadMcp",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
restartMcpServer: {
|
||||
name: "restartMcpServer",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.7.0
|
||||
// protoc v3.19.1
|
||||
// source: slash.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { Empty, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
/** SlashService provides methods for managing slash */
|
||||
export type SlashServiceDefinition = typeof SlashServiceDefinition
|
||||
export const SlashServiceDefinition = {
|
||||
name: "SlashService",
|
||||
fullName: "cline.SlashService",
|
||||
methods: {
|
||||
/** Sends button click message */
|
||||
reportBug: {
|
||||
name: "reportBug",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
condense: {
|
||||
name: "condense",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, EmptyRequest, StringRequest } from "./common"
|
||||
import { EmptyRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -93,22 +93,6 @@ export const StateServiceDefinition = {
|
||||
responseStream: true,
|
||||
options: {},
|
||||
},
|
||||
toggleFavoriteModel: {
|
||||
name: "toggleFavoriteModel",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
resetState: {
|
||||
name: "resetState",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
+2
-163
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, EmptyRequest, Int64Request, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
import { Empty, EmptyRequest, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -50,7 +50,6 @@ export interface GetTaskHistoryRequest {
|
||||
favoritesOnly: boolean
|
||||
searchQuery: string
|
||||
sortBy: string
|
||||
currentWorkspaceOnly: boolean
|
||||
}
|
||||
|
||||
/** Response for task history */
|
||||
@@ -73,14 +72,6 @@ export interface TaskItem {
|
||||
cacheReads: number
|
||||
}
|
||||
|
||||
/** Request for ask response operation */
|
||||
export interface AskResponseRequest {
|
||||
metadata?: Metadata | undefined
|
||||
responseType: string
|
||||
text: string
|
||||
images: string[]
|
||||
}
|
||||
|
||||
function createBaseNewTaskRequest(): NewTaskRequest {
|
||||
return { metadata: undefined, text: "", images: [] }
|
||||
}
|
||||
@@ -559,7 +550,7 @@ export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksR
|
||||
}
|
||||
|
||||
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "", currentWorkspaceOnly: false }
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
|
||||
}
|
||||
|
||||
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
@@ -576,9 +567,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
writer.uint32(34).string(message.sortBy)
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
writer.uint32(40).bool(message.currentWorkspaceOnly)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -621,14 +609,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.sortBy = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.currentWorkspaceOnly = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -644,7 +624,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
|
||||
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
|
||||
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
|
||||
currentWorkspaceOnly: isSet(object.currentWorkspaceOnly) ? globalThis.Boolean(object.currentWorkspaceOnly) : false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -662,9 +641,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
obj.sortBy = message.sortBy
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
obj.currentWorkspaceOnly = message.currentWorkspaceOnly
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -678,7 +654,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.favoritesOnly = object.favoritesOnly ?? false
|
||||
message.searchQuery = object.searchQuery ?? ""
|
||||
message.sortBy = object.sortBy ?? ""
|
||||
message.currentWorkspaceOnly = object.currentWorkspaceOnly ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -974,115 +949,6 @@ export const TaskItem: MessageFns<TaskItem> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseAskResponseRequest(): AskResponseRequest {
|
||||
return { metadata: undefined, responseType: "", text: "", images: [] }
|
||||
}
|
||||
|
||||
export const AskResponseRequest: MessageFns<AskResponseRequest> = {
|
||||
encode(message: AskResponseRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.responseType !== "") {
|
||||
writer.uint32(18).string(message.responseType)
|
||||
}
|
||||
if (message.text !== "") {
|
||||
writer.uint32(26).string(message.text)
|
||||
}
|
||||
for (const v of message.images) {
|
||||
writer.uint32(34).string(v!)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AskResponseRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseAskResponseRequest()
|
||||
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.responseType = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.text = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.images.push(reader.string())
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): AskResponseRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
responseType: isSet(object.responseType) ? globalThis.String(object.responseType) : "",
|
||||
text: isSet(object.text) ? globalThis.String(object.text) : "",
|
||||
images: globalThis.Array.isArray(object?.images) ? object.images.map((e: any) => globalThis.String(e)) : [],
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: AskResponseRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.responseType !== "") {
|
||||
obj.responseType = message.responseType
|
||||
}
|
||||
if (message.text !== "") {
|
||||
obj.text = message.text
|
||||
}
|
||||
if (message.images?.length) {
|
||||
obj.images = message.images
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AskResponseRequest>, I>>(base?: I): AskResponseRequest {
|
||||
return AskResponseRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AskResponseRequest>, I>>(object: I): AskResponseRequest {
|
||||
const message = createBaseAskResponseRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.responseType = object.responseType ?? ""
|
||||
message.text = object.text ?? ""
|
||||
message.images = object.images?.map((e) => e) || []
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type TaskServiceDefinition = typeof TaskServiceDefinition
|
||||
export const TaskServiceDefinition = {
|
||||
name: "TaskService",
|
||||
@@ -1169,33 +1035,6 @@ export const TaskServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Sends a response to a previous ask operation */
|
||||
askResponse: {
|
||||
name: "askResponse",
|
||||
requestType: AskResponseRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Records task feedback (thumbs up/down) */
|
||||
taskFeedback: {
|
||||
name: "taskFeedback",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Shows task completion changes diff in a view */
|
||||
taskCompletionViewChanges: {
|
||||
name: "taskCompletionViewChanges",
|
||||
requestType: Int64Request,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -15,15 +15,6 @@ export interface IsImageUrl {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface OpenGraphData {
|
||||
title: string
|
||||
description: string
|
||||
image: string
|
||||
url: string
|
||||
siteName: string
|
||||
type: string
|
||||
}
|
||||
|
||||
function createBaseIsImageUrl(): IsImageUrl {
|
||||
return { isImage: false, url: "" }
|
||||
}
|
||||
@@ -100,146 +91,6 @@ export const IsImageUrl: MessageFns<IsImageUrl> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseOpenGraphData(): OpenGraphData {
|
||||
return { title: "", description: "", image: "", url: "", siteName: "", type: "" }
|
||||
}
|
||||
|
||||
export const OpenGraphData: MessageFns<OpenGraphData> = {
|
||||
encode(message: OpenGraphData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.title !== "") {
|
||||
writer.uint32(10).string(message.title)
|
||||
}
|
||||
if (message.description !== "") {
|
||||
writer.uint32(18).string(message.description)
|
||||
}
|
||||
if (message.image !== "") {
|
||||
writer.uint32(26).string(message.image)
|
||||
}
|
||||
if (message.url !== "") {
|
||||
writer.uint32(34).string(message.url)
|
||||
}
|
||||
if (message.siteName !== "") {
|
||||
writer.uint32(42).string(message.siteName)
|
||||
}
|
||||
if (message.type !== "") {
|
||||
writer.uint32(50).string(message.type)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenGraphData {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseOpenGraphData()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.title = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.description = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.image = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.url = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.siteName = reader.string()
|
||||
continue
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break
|
||||
}
|
||||
|
||||
message.type = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): OpenGraphData {
|
||||
return {
|
||||
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||
description: isSet(object.description) ? globalThis.String(object.description) : "",
|
||||
image: isSet(object.image) ? globalThis.String(object.image) : "",
|
||||
url: isSet(object.url) ? globalThis.String(object.url) : "",
|
||||
siteName: isSet(object.siteName) ? globalThis.String(object.siteName) : "",
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: OpenGraphData): unknown {
|
||||
const obj: any = {}
|
||||
if (message.title !== "") {
|
||||
obj.title = message.title
|
||||
}
|
||||
if (message.description !== "") {
|
||||
obj.description = message.description
|
||||
}
|
||||
if (message.image !== "") {
|
||||
obj.image = message.image
|
||||
}
|
||||
if (message.url !== "") {
|
||||
obj.url = message.url
|
||||
}
|
||||
if (message.siteName !== "") {
|
||||
obj.siteName = message.siteName
|
||||
}
|
||||
if (message.type !== "") {
|
||||
obj.type = message.type
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<OpenGraphData>, I>>(base?: I): OpenGraphData {
|
||||
return OpenGraphData.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<OpenGraphData>, I>>(object: I): OpenGraphData {
|
||||
const message = createBaseOpenGraphData()
|
||||
message.title = object.title ?? ""
|
||||
message.description = object.description ?? ""
|
||||
message.image = object.image ?? ""
|
||||
message.url = object.url ?? ""
|
||||
message.siteName = object.siteName ?? ""
|
||||
message.type = object.type ?? ""
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type WebServiceDefinition = typeof WebServiceDefinition
|
||||
export const WebServiceDefinition = {
|
||||
name: "WebService",
|
||||
@@ -253,14 +104,6 @@ export const WebServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
fetchOpenGraphData: {
|
||||
name: "fetchOpenGraphData",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: OpenGraphData,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Public PostHog key (safe for open source)
|
||||
export const posthogConfig = {
|
||||
apiKey: "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K",
|
||||
host: "https://data.cline.bot",
|
||||
uiHost: "https://us.posthog.com",
|
||||
host: "https://us.i.posthog.com",
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user