mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8456f45a80 | |||
| e288bb9e4f | |||
| a369551a9e | |||
| b0fa1be2df | |||
| 793db91291 | |||
| 9cb0e51ffc | |||
| d68bf862a8 | |||
| c2f69737aa | |||
| 99b8b92b27 | |||
| 1239fe1b33 | |||
| 062e597b74 | |||
| d099be10fd | |||
| 3e8268605f | |||
| 1615bf3f82 | |||
| 15f713d26a | |||
| 5babd9e061 | |||
| ba12672600 | |||
| 163bf77677 | |||
| edf5ea00f6 | |||
| c3c80ffc4d | |||
| f14ed8506d | |||
| 41202df74e | |||
| ab88599e05 | |||
| c2dc0e531c | |||
| 246b0fa999 | |||
| 6c099fbe12 | |||
| 00bf05c26c | |||
| da99e2bf4b | |||
| 963e2c00eb | |||
| 9355d61bc1 | |||
| a9e17fee57 | |||
| 3e8548a341 | |||
| 76b86ff0c0 | |||
| 684438b44c | |||
| 3c84388fb2 | |||
| b0f86201d2 | |||
| d1fc59758e | |||
| 87c9f58902 | |||
| cda3eb8236 | |||
| 3c21d2be1f | |||
| f3adf68775 | |||
| 6bd3181133 | |||
| dd3a234a69 | |||
| c496f8a90d | |||
| 25b1cf91fc | |||
| b54e2043fe | |||
| 98e5ccc547 | |||
| 813a9589d0 | |||
| 64eb66d49a | |||
| 8c17d864c8 | |||
| 4f931c2d9d | |||
| 5836db3093 | |||
| 18879edf9f | |||
| 1cc702c8b9 | |||
| c8caa6f9b9 | |||
| 01f61b6765 | |||
| b8dd6abe61 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: Return the updated token
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude Sonnet 4. Cline uses complex prompts so less capable models may not work as expected.
|
||||
**Important:** All bug reports must be reproducible using Claude Sonnet 4.5. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: dropdown
|
||||
id: plugin-type
|
||||
attributes:
|
||||
@@ -49,7 +49,7 @@ body:
|
||||
attributes:
|
||||
label: Provider/Model
|
||||
description: What provider and model were you using when the issue occurred?
|
||||
placeholder: 'e.g., cline:anthropic/claude-sonnet-4, gemini:gemini-2.5-pro-exp-03-25'
|
||||
placeholder: 'e.g., cline:anthropic/claude-sonnet-4.5, gemini:gemini-2.5-pro-exp-03-25'
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
+82
-151
@@ -1,6 +1,9 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -14,7 +17,45 @@ permissions:
|
||||
pull-requests: write # Needed to add comments/annotations to PRs
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -33,18 +74,6 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Setup Python for coverage script
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
@@ -52,7 +81,6 @@ jobs:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
@@ -68,60 +96,60 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
- 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
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
|
||||
- name: Format Check
|
||||
run: npm run format
|
||||
|
||||
# Build the extension before running tests
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
run: npm run ci:build
|
||||
|
||||
- name: Unit Tests (with coverage on Linux)
|
||||
run: |
|
||||
if [ "${{ runner.os }}" = "Linux" ]; then
|
||||
npm install --no-save nyc
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
else
|
||||
npm run test:unit
|
||||
fi
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
id: extension_coverage
|
||||
- name: Unit Tests with coverage - Linux
|
||||
id: unit_tests_linux
|
||||
continue-on-error: true
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
|
||||
# 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
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
continue-on-error: true
|
||||
if: runner.os != 'Linux'
|
||||
run: |
|
||||
npm run test:unit
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
continue-on-error: true
|
||||
if: runner.os == 'Linux'
|
||||
run: xvfb-run -a npm run test:coverage
|
||||
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
continue-on-error: true
|
||||
if: runner.os != 'Linux'
|
||||
run: npm run test:integration
|
||||
|
||||
# Run webview tests with coverage
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_coverage
|
||||
id: webview_tests
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage 2>&1 | tee webview_coverage.txt
|
||||
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
|
||||
npm run test:coverage
|
||||
|
||||
- name: Check Test Results
|
||||
if: always()
|
||||
run: |
|
||||
failed=""
|
||||
[[ "${{ steps.unit_tests_linux.outcome }}" == "failure" && "${{ runner.os }}" == "Linux" ]] && failed="$failed unit_tests_linux"
|
||||
[[ "${{ steps.unit_tests_non_linux.outcome }}" == "failure" && "${{ runner.os }}" != "Linux" ]] && failed="$failed unit_tests_non_linux"
|
||||
[[ "${{ steps.integration_tests_linux.outcome }}" == "failure" && "${{ runner.os }}" == "Linux" ]] && failed="$failed integration_tests_linux"
|
||||
[[ "${{ steps.integration_tests_non_linux.outcome }}" == "failure" && "${{ runner.os }}" != "Linux" ]] && failed="$failed integration_tests_non_linux"
|
||||
[[ "${{ steps.webview_tests.outcome }}" == "failure" ]] && failed="$failed webview_tests"
|
||||
[[ -n "$failed" ]] && { echo "❌ The following test suites failed:$failed"; exit 1; }
|
||||
echo "✅ All tests passed"
|
||||
|
||||
# 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
|
||||
@@ -129,27 +157,11 @@ jobs:
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
extension_coverage.txt
|
||||
webview-ui/webview_coverage.txt
|
||||
coverage-unit/lcov.info
|
||||
webview-ui/coverage/lcov.info
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
# 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" ]; then
|
||||
echo "Extension Integration Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Webview Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test-platform-integration:
|
||||
needs: quality-checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -159,7 +171,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
@@ -212,85 +224,6 @@ jobs:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: coverage/**/lcov.info
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on PRs to main branch
|
||||
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for accurate comparison
|
||||
|
||||
# Setup Python for coverage script
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
|
||||
# Download coverage artifacts from test job
|
||||
- name: Download Coverage Reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: . # Download to root directory to match expected paths
|
||||
|
||||
# Process coverage workflow
|
||||
- name: Process coverage workflow
|
||||
id: coverage
|
||||
run: |
|
||||
# Extract PR number from GITHUB_REF
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed -e 's/refs\/pull\///' -e 's/\/merge//')
|
||||
|
||||
# Run the coverage workflow from root directory
|
||||
PYTHONPATH=.github/scripts python -m coverage_check process-workflow \
|
||||
--base-branch ${{ github.base_ref }} \
|
||||
--pr-number $PR_NUMBER \
|
||||
--repo $GITHUB_REPOSITORY \
|
||||
--token ${{ secrets.GITHUB_TOKEN }} \
|
||||
--verbose
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
qlty:
|
||||
needs: [test, test-platform-integration]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -298,8 +231,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for accurate comparison
|
||||
|
||||
- name: Download unit tests coverage reports
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -30,17 +30,19 @@ jobs:
|
||||
-H "User-Agent: cline-pr-trigger" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://api.github.com/repos/cline/intellij-plugin/dispatches \
|
||||
-d '{
|
||||
"event_type": "cline-pr-check",
|
||||
"client_payload": {
|
||||
"pr_number": "${{ github.event.number }}",
|
||||
"branch_name": "${{ github.head_ref }}",
|
||||
"action": "${{ github.event.action }}",
|
||||
"sha": "${{ github.event.pull_request.head.sha }}",
|
||||
"pr_title": "${{ github.event.pull_request.title }}",
|
||||
"pr_url": "${{ github.event.pull_request.html_url }}"
|
||||
}
|
||||
}'
|
||||
-d @- <<EOF
|
||||
{
|
||||
"event_type": "cline-pr-check",
|
||||
"client_payload": {
|
||||
"pr_number": "${{ github.event.number }}",
|
||||
"branch_name": "${{ github.head_ref }}",
|
||||
"action": "${{ github.event.action }}",
|
||||
"sha": "${{ github.event.pull_request.head.sha }}",
|
||||
"pr_title": ${{ toJSON(github.event.pull_request.title) }},
|
||||
"pr_url": "${{ github.event.pull_request.html_url }}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Log trigger details
|
||||
run: |
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## [3.32.6]
|
||||
|
||||
- Add experimental support for VSCode multi root workspaces
|
||||
- Add Claude Sonnet 4.5 to Claude Code provider
|
||||
- Add Glm 4.6 to Z AI provider
|
||||
|
||||
## [3.32.5]
|
||||
|
||||
- Improve thinking budget slider UI to take up less space
|
||||
- Fix Vercel provider cost note and sign-up url
|
||||
- Fix repeated API error 400 in SAP AI Core provider
|
||||
- Add us-west-1 to Amazon Bedrock regions
|
||||
- Fix OCA provider refresh logic
|
||||
|
||||
## [3.32.4]
|
||||
|
||||
- Add 1m context window support to Claude Sonnet 4.5
|
||||
- Add Claude Sonnet 4.5 to GCP Vertex
|
||||
- Add prompt caching support for OpenRouter accidental `anthropic/claude-4.5-sonnet` model ID
|
||||
|
||||
## [3.32.3]
|
||||
|
||||
- Add Claude Sonnet 4.5 to Bedrock provider
|
||||
- Add Alert banner for new Claude Sonnet 4.5 model
|
||||
|
||||
## [3.32.2]
|
||||
|
||||
- Add Claude Sonnet 4.5 to Cline/OpenRouter/Anthropic providers
|
||||
- Add /task deep link handler
|
||||
|
||||
## [3.32.1]
|
||||
|
||||
- Preserve reasoning traces for Cline/OpenRouter/Anthropic providers to maintain conversation integrity
|
||||
|
||||
@@ -74,7 +74,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
4. Testing
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
- Run `npm run test:ci` to run tests locally
|
||||
|
||||
### Extension
|
||||
|
||||
|
||||
+32
-26
@@ -25,15 +25,15 @@
|
||||
},
|
||||
"styling": {
|
||||
"eyebrows": "breadcrumbs",
|
||||
"codeblocks": "system"
|
||||
"codeblocks": "system",
|
||||
"css": "styles.css"
|
||||
},
|
||||
"appearance": {
|
||||
"default": "system",
|
||||
"strict": false
|
||||
},
|
||||
"fonts": {
|
||||
"family": "Roboto",
|
||||
"weight": 400
|
||||
"family": "Roboto"
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
@@ -58,9 +58,8 @@
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"getting-started/what-is-cline",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/installing-cline-jetbrains",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
{
|
||||
@@ -82,16 +81,6 @@
|
||||
{
|
||||
"group": "Features",
|
||||
"pages": [
|
||||
"features/auto-approve",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
"features/drag-and-drop",
|
||||
"features/plan-and-act",
|
||||
"features/slash-commands/workflows",
|
||||
"features/focus-chain",
|
||||
"features/auto-compact",
|
||||
"features/editing-messages",
|
||||
"features/dictation",
|
||||
{
|
||||
"group": "@ Mentions",
|
||||
"pages": [
|
||||
@@ -103,16 +92,10 @@
|
||||
"features/at-mentions/url-mentions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
"features/slash-commands/new-task",
|
||||
"features/slash-commands/new-rule",
|
||||
"features/slash-commands/smol",
|
||||
"features/slash-commands/report-bug",
|
||||
"features/slash-commands/deep-planning"
|
||||
]
|
||||
},
|
||||
"features/auto-approve",
|
||||
"features/auto-compact",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -129,7 +112,24 @@
|
||||
"features/customization/opening-cline-in-sidebar",
|
||||
"features/customization/disable-terminal-pagers"
|
||||
]
|
||||
}
|
||||
},
|
||||
"features/dictation",
|
||||
"features/drag-and-drop",
|
||||
"features/editing-messages",
|
||||
"features/focus-chain",
|
||||
"features/plan-and-act",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
"features/slash-commands/new-task",
|
||||
"features/slash-commands/new-rule",
|
||||
"features/slash-commands/smol",
|
||||
"features/slash-commands/report-bug",
|
||||
"features/slash-commands/deep-planning"
|
||||
]
|
||||
},
|
||||
"features/slash-commands/workflows",
|
||||
"features/yolo-mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -232,6 +232,12 @@
|
||||
"url": "getting-started/what-is-cline"
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/getting-started/installing-cline-jetbrains",
|
||||
"destination": "/getting-started/installing-cline"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
"prompt": "Search Cline documentation..."
|
||||
},
|
||||
|
||||
@@ -21,7 +21,6 @@ While Cline has only a few default keyboard shortcuts, you can assign your own s
|
||||
|
||||
| Command ID | Description |
|
||||
| ---------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| [`cline.openInNewTab`](/features/commands-and-shortcuts/overview) | Opens Cline in a new editor tab |
|
||||
| [`cline.addToChat`](/features/commands-and-shortcuts/code-commands) | Adds selected code to Cline chat |
|
||||
| [`cline.addTerminalOutputToChat`](/features/commands-and-shortcuts/terminal-integration) | Adds terminal output to Cline |
|
||||
| `cline.focusChatInput` | Focuses the Cline chat input field |
|
||||
|
||||
+121
-32
@@ -1,60 +1,149 @@
|
||||
---
|
||||
title: Dictation
|
||||
description:
|
||||
title: "Dictation"
|
||||
description: "Communicate with Cline using your voice for faster, more natural AI collaboration"
|
||||
---
|
||||
|
||||
Cline lets you transcribe speech to text in an easy, built-in service
|
||||
Dictation transforms how you work with AI. Instead of typing out complex thoughts, you speak naturally and share your complete intent. This isn't just about speed - though voice is faster - it's about unlocking the kind of fluid collaboration that typing can't match.
|
||||
|
||||
## Get Started
|
||||
## Why Voice Changes Everything
|
||||
|
||||
1. **Enable Dictation** in Feature Settings.
|
||||
2. **Click the microphone** in the chat input area.
|
||||
3. **Speak** - the button turns red while recording.
|
||||
4. **Click Stop Recording** when done.
|
||||
5. **Wait for transcription** - "[Transcribing...]" will appear and then the finished transcription will appear.
|
||||
When you type, you edit yourself. You simplify complex ideas, skip context, and lose nuance. When you speak, you share everything on your mind - the full problem, the constraints, the edge cases you're worried about.
|
||||
|
||||
## Settings
|
||||
Use Dictation constantly in [Plan mode](/features/plan-and-act) for rapid back-and-forth discussions. Instead of typing careful, structured prompts, think about a problem. Cline asks clarifying questions, respond immediately, and iterate until having a solid plan.
|
||||
|
||||
Enable or disable dictation in Feature Settings by toggling "Enable Dictation." You can also change the transcription language to one of the supported languages.
|
||||
The friction of typing was holding back real collaboration. Voice removes that friction.
|
||||
|
||||
## Requirements
|
||||
## Getting Started
|
||||
|
||||
Cline uses FFmpeg to capture your voice across all platforms:
|
||||
**Enable Dictation:**
|
||||
1. Go to Settings → Features → Dictation
|
||||
2. Toggle "Enable Dictation" on
|
||||
3. Sign into your Cline account when prompted
|
||||
4. Install FFmpeg if you haven't already (Cline will guide you)
|
||||
|
||||
- **macOS**: FFmpeg (via Homebrew: `brew install ffmpeg`)
|
||||
- **Linux**: FFmpeg (via apt: `sudo apt-get install ffmpeg`)
|
||||
- **Windows**: FFmpeg (via winget: `winget install Gyan.FFmpeg`)
|
||||
Once enabled, you'll see a microphone button in the chat input area.
|
||||
|
||||
**Using Dictation:**
|
||||
- Click the microphone button to start recording
|
||||
- Speak naturally
|
||||
- Click again to stop recording
|
||||
- Wait for transcription to appear in the chat
|
||||
|
||||
<Tip>
|
||||
Dictation works with any AI model you've configured. The transcription happens through Cline's service, but your conversation continues with whatever model you're using.
|
||||
</Tip>
|
||||
|
||||
## System Requirements
|
||||
|
||||
Dictation uses FFmpeg to capture your voice across all platforms:
|
||||
|
||||
- **macOS**: FFmpeg (via Homebrew: `brew install ffmpeg`)
|
||||
- **Linux**: FFmpeg (via apt: `sudo apt-get install ffmpeg`)
|
||||
- **Windows**: FFmpeg (via winget: `winget install Gyan.FFmpeg`)
|
||||
|
||||
If you don't have FFmpeg installed, Cline will automatically detect this and prompt you to install it with a single click.
|
||||
|
||||
## Technical Details
|
||||
## Where Dictation Shines
|
||||
|
||||
### Independent from Chat Provider
|
||||
### Plan Mode Conversations
|
||||
|
||||
The voice transcription feature works completely independently from whatever chat provider you're using. You can use Claude, GPT-4, or any other model for your conversations, and voice transcription will always use Cline's own transcription service. As long as you have a valid Cline account with enough credits, dictation will work regardless of your chat model choice.
|
||||
Dictation is perfect for [Plan mode](/features/plan-and-act) discussions. Instead of carefully crafting prompts, you can:
|
||||
|
||||
### Audio Format
|
||||
- Dictate your entire problem context in one go
|
||||
- Respond to Cline's questions immediately
|
||||
- Iterate on ideas without typing friction
|
||||
- Think out loud while Cline listens
|
||||
|
||||
Voice recordings are captured in WebM format using the Opus codec for optimal compression. The system records in mono (single channel) at a 16kHz sample rate, which is specifically optimized for voice recognition. The bitrate is set to 32kbps to keep file sizes efficient while maintaining good audio quality.
|
||||
Start a planning session by speaking for 2-3 minutes straight, explaining the full context of what you're trying to build, the constraints you're working with, and the specific challenges you're facing.
|
||||
|
||||
### Privacy & Security
|
||||
### Complex Problem Explanation
|
||||
|
||||
Your audio is recorded locally on your machine and only the audio file itself is sent to Cline's transcription service for processing. No audio is stored anywhere after transcription is complete, and all temporary files are automatically cleaned up to protect your privacy.
|
||||
Some problems are hard to type out. When you're dealing with:
|
||||
- Multi-step workflows with edge cases
|
||||
- Integration challenges across multiple systems
|
||||
- Performance issues with specific reproduction steps
|
||||
- UI/UX problems that need detailed context
|
||||
|
||||
Speaking lets you explain the full situation naturally, including all the "oh, and also..." details that matter.
|
||||
|
||||
### Code Review and Debugging
|
||||
|
||||
When reviewing code or explaining bugs, voice lets you walk through your thought process:
|
||||
- "This function looks fine, but I'm worried about what happens when..."
|
||||
- "The issue might be in this section, or possibly this other area..."
|
||||
- "I tried X and Y, but neither worked because..."
|
||||
|
||||
You can share your complete debugging journey instead of just the final question.
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
**System Requirements:**
|
||||
- FFmpeg installed on your system
|
||||
- Active internet connection
|
||||
- Cline account with transcription credits
|
||||
|
||||
**Audio Quality:**
|
||||
- Records in WebM format with Opus codec
|
||||
- Mono audio at 16kHz sample rate
|
||||
- Optimized for voice recognition
|
||||
|
||||
**Privacy:**
|
||||
- Audio recorded locally on your machine
|
||||
- Only audio files sent for transcription
|
||||
- No audio stored after transcription
|
||||
- Temporary files automatically cleaned up
|
||||
|
||||
## Cost and Credits
|
||||
|
||||
Voice transcription costs $0.006 per minute through your Cline account. For most users, this works out to pennies per session.
|
||||
|
||||
A typical 5-minute planning conversation costs about 3 cents. Even heavy voice users rarely spend more than a few dollars per month.
|
||||
|
||||
<Note>
|
||||
Pricing is experimental and may change as we refine the service.
|
||||
</Note>
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Speak Naturally**
|
||||
Don't try to speak like you type. Use your normal conversational tone and don't worry about perfect grammar.
|
||||
|
||||
**Give Context First**
|
||||
Start with the big picture, then drill down into specifics. "I'm building a React app that needs to handle real-time data, and I'm running into performance issues with the WebSocket connection..."
|
||||
|
||||
**Use Voice for Exploration**
|
||||
Dictation is perfect for exploratory conversations where you're not sure exactly what you need. Start talking through the problem and let the conversation evolve.
|
||||
|
||||
**Combine with Text**
|
||||
You don't have to use voice for everything. Use voice for complex explanations and context, then switch to text for quick follow-ups or code snippets.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
`Failed to start recording` - Audio recording tools aren't installed. Cline will prompt you to install FFmpeg - just follow the chat instructions.
|
||||
**Microphone Not Working**
|
||||
- Check your IDE permissions for microphone access
|
||||
- Ensure FFmpeg is properly installed
|
||||
- Try refreshing VSCode/your editor
|
||||
|
||||
`Invalid audio format or request data` - Usually an audio recording issue. Make sure FFmpeg is properly installed and working.
|
||||
**Poor Transcription Quality**
|
||||
- Speak clearly and at normal volume
|
||||
- Reduce background noise if possible
|
||||
- Check your microphone settings
|
||||
|
||||
`Authentication failed` - You need to reauthenticate your Cline account. Sign out and back in, then check your internet connection.
|
||||
**Connection Issues**
|
||||
- Verify internet connection
|
||||
- Check if firewall is blocking Cline's servers
|
||||
- Try signing out and back into your Cline account
|
||||
|
||||
`Insufficient credits for transcription service` - Your Cline account doesn't have enough credits. Check your balance and purchase more if needed.
|
||||
**Authentication Issues**
|
||||
- Sign out and back into your Cline account if you see authentication errors
|
||||
- Check that your account has sufficient transcription credits
|
||||
- Verify your internet connection is stable
|
||||
|
||||
`Cannot connect to transcription service` - Connection issue. Check your internet connection and firewall settings aren't blocking Cline's servers.
|
||||
**Audio Recording Issues**
|
||||
- Ensure FFmpeg is properly installed and accessible
|
||||
- Check that your browser/IDE has microphone permissions
|
||||
- Try restarting your editor if audio capture fails
|
||||
|
||||
## API Usage
|
||||
## The Future of AI Collaboration
|
||||
|
||||
Voice transcription uses Cline's transcription service, which requires credits from your Cline account. Currently, voice transcription is billed at $0.006 per minute of audio.
|
||||
|
||||
**Note:** We are still experimenting with this feature and pricing may change in the future.
|
||||
When you can speak your thoughts as fast as you think them, you stop self-editing. You share the full context, the edge cases, the "what if" scenarios that matter. This leads to better solutions and fewer back-and-forth clarifications.
|
||||
|
||||
@@ -24,6 +24,10 @@ Plan mode is where you and Cline figure out what you're trying to build and how
|
||||
- Focuses on understanding requirements and creating a strategy
|
||||
- Helps identify potential issues before you write a single line of code
|
||||
|
||||
<Tip>
|
||||
Try [Dictation](/features/dictation) in Plan mode - instead of typing out complex requirements, you can speak naturally and share your complete thought process. It's perfect for rapid back-and-forth planning discussions.
|
||||
</Tip>
|
||||
|
||||
#### Act Mode: Build It
|
||||
|
||||
Once you've got a plan, you switch to Act mode. Now Cline:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "YOLO Mode"
|
||||
sidebarTitle: "YOLO Mode"
|
||||
---
|
||||
|
||||
YOLO mode is exactly what it sounds like - Cline auto-approves everything. Check the box in feature settings and he'll execute file changes, terminal commands, even transitions from Plan to Act mode without asking.
|
||||
|
||||
Think of it as [Auto Approve](/features/auto-approve) on steroids - instead of granular permissions, YOLO mode gives Cline complete autonomy.
|
||||
|
||||
<Note>
|
||||
**Warning: This is dangerous.** YOLO mode disables all safety checks. Cline will execute whatever he decides without asking permission.
|
||||
</Note>
|
||||
|
||||
## What Gets Auto-Approved
|
||||
|
||||
When YOLO mode is enabled, Cline automatically approves:
|
||||
|
||||
- **All file operations** - reading, writing, and modifying files anywhere on your system
|
||||
- **All terminal commands** - including potentially destructive operations
|
||||
- **Browser actions** - web scraping, form submissions, navigation
|
||||
- **MCP server tools** - external integrations and API calls
|
||||
- **Mode transitions** - automatic switching from Plan to Act mode
|
||||
|
||||
Essentially, every safety guardrail is removed. Cline operates with complete autonomy.
|
||||
|
||||
## How to Enable YOLO Mode
|
||||
|
||||
Navigate to Cline Settings → Features and check the "YOLO Mode" box. That's it - no confirmation dialogs, no additional warnings. Once enabled, Cline will start auto-approving all actions immediately.
|
||||
|
||||
To disable it, simply uncheck the box. Any pending actions will still require your approval once YOLO mode is turned off.
|
||||
|
||||
## When You Might Use This
|
||||
|
||||
YOLO mode was built primarily for our upcoming scriptable CLI where fully autonomous execution makes sense. In the GUI, you might consider it for:
|
||||
|
||||
**Rapid prototyping** where you want zero friction and don't care about potential mistakes. Perfect for throwaway experiments or exploring new ideas quickly.
|
||||
|
||||
**Trusted, repetitive tasks** where you've already validated Cline's approach and want to eliminate approval overhead. Think routine refactoring or well-established patterns.
|
||||
|
||||
**Demonstration purposes** where you want to show Cline's capabilities without constant interruptions.
|
||||
|
||||
## What Could Go Wrong
|
||||
|
||||
Since YOLO mode removes all safety checks, Cline could:
|
||||
|
||||
- Delete important files without warning
|
||||
- Execute commands that modify system settings
|
||||
- Make network requests to external services
|
||||
- Overwrite configuration files
|
||||
- Install or uninstall software packages
|
||||
- Commit and push changes to version control
|
||||
|
||||
The risk level depends entirely on what you ask Cline to do. Simple tasks remain relatively safe, but complex requests can have unpredictable consequences.
|
||||
|
||||
## Best Practices
|
||||
|
||||
If you decide to use YOLO mode:
|
||||
|
||||
**Start with isolated environments.** Use it in throwaway projects or sandboxed environments first. Never enable it on production codebases until you understand the risks.
|
||||
|
||||
**Be specific with requests.** Vague instructions combined with unlimited permissions can lead to unexpected results. The clearer your requirements, the more predictable Cline's actions.
|
||||
|
||||
**Monitor the output.** Even though Cline doesn't ask for permission, he still shows you what he's doing. Watch the terminal output and file changes as they happen.
|
||||
|
||||
**Keep version control handy.** Make sure you can easily revert changes if something goes wrong. Git becomes your safety net when YOLO mode is your workflow.
|
||||
|
||||
## Inspiration: What Becomes Possible
|
||||
|
||||
With YOLO mode enabled, you can:
|
||||
|
||||
**Build entire applications** from a single prompt. Describe what you want and let Cline handle everything - file creation, dependency installation, configuration setup, even deployment scripts.
|
||||
|
||||
**Automate complex workflows** that normally require dozens of approval clicks. Data processing pipelines, build system setup, or multi-step refactoring operations become seamless.
|
||||
|
||||
**Rapid iteration cycles** where you can quickly test ideas without approval friction. Perfect for exploring different approaches or experimenting with new technologies.
|
||||
|
||||
**Live demonstrations** where you can show Cline's full capabilities without stopping to approve every action. Great for presentations or teaching scenarios.
|
||||
|
||||
The key is understanding that YOLO mode transforms Cline from an interactive assistant into an autonomous agent. Use that power wisely.
|
||||
|
||||
---
|
||||
|
||||
Questions or feedback? Reach us in our [Discord](https://discord.gg/cline) or [r/cline](https://reddit.com/r/cline).
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
title: "Installing Cline for JetBrains"
|
||||
description: "Install Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-logo.svg"
|
||||
alt="JetBrains logo"
|
||||
style={{ width: "200px", height: "auto", margin: "0 auto 20px auto", display: "block" }}
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Cline for JetBrains works almost identically to Cline in VSCode. All the core features work properly: diff editing, using tools, logging in with different providers, MCP servers, Cline rules and workflows, and more.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-demo-hifi.gif"
|
||||
alt="Cline running in JetBrains IDE showing AI assistance"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Cline is now available on the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline). Works in IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and all other JetBrains IDEs.
|
||||
|
||||
## Installation
|
||||
|
||||
**Method 1: Direct from your IDE**
|
||||
|
||||
1. Open your JetBrains IDE
|
||||
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS)
|
||||
3. Go to Plugins → Marketplace tab
|
||||
4. Search "Cline" and click Install
|
||||
5. Restart your IDE
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
**Method 2: Browser install**
|
||||
|
||||
Visit [plugins.jetbrains.com/plugin/28247-cline](https://plugins.jetbrains.com/plugin/28247-cline) and click the "Install to IDE" button. Your IDE will open and prompt you to install.
|
||||
|
||||
<details>
|
||||
<summary>Method 3: Manual installation</summary>
|
||||
|
||||
Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline), then:
|
||||
|
||||
1. Go to Settings → Plugins
|
||||
2. Click the gear icon → Install Plugin from Disk
|
||||
3. Select the downloaded `.zip` file
|
||||
4. Restart your IDE
|
||||
|
||||
</details>
|
||||
|
||||
## Getting Started with Cline
|
||||
|
||||
After installation, you'll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to **View** → **Tool Windows** → **Cline**.
|
||||
|
||||
Sign in is optional - you can also bring your own API key. If you want to sign in, click **Sign In** in the Cline panel. You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account. No credit card needed to get started with free credits.
|
||||
|
||||
Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
|
||||
|
||||
## Key Differences from VSCode
|
||||
|
||||
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the **Command Output** section to see results.
|
||||
|
||||
## What Works
|
||||
|
||||
Everything else works exactly like VSCode. Cline can read, write, and edit files with the same precision. All his tools work identically - file operations, web browsing, you name it.
|
||||
|
||||
You get full support for:
|
||||
- API providers (Anthropic, OpenAI, local models)
|
||||
- MCP servers and custom tools
|
||||
- Cline rules and workflows
|
||||
- @ mentions for files, folders, and problems
|
||||
- Drag & drop for files and images
|
||||
|
||||
## Tips for JetBrains Users
|
||||
|
||||
Cline automatically understands your project structure, just like in VSCode. He works with any language your JetBrains IDE supports - Java, Python, JavaScript, Go, whatever you're building.
|
||||
|
||||
I find it helpful to share error messages and stack traces directly in the chat when debugging. You can also ask him to review your code changes before committing.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Installation Issues
|
||||
|
||||
If you can't find Cline in the marketplace:
|
||||
- Make sure you're searching in the **Marketplace** tab (not Installed)
|
||||
- Try searching for "Cline AI" or just "Cline"
|
||||
- Check that your IDE version is compatible (2023.1 or later recommended)
|
||||
|
||||
If installation fails:
|
||||
- Restart your IDE and try again
|
||||
- Check your internet connection
|
||||
- Try installing from disk as an alternative
|
||||
|
||||
### Plugin Not Appearing
|
||||
|
||||
If you don't see the Cline tool window after installation:
|
||||
- Restart your IDE completely (File → Exit and reopen)
|
||||
- Check **View** → **Tool Windows** → **Cline**
|
||||
- Verify the plugin is enabled in **Settings** → **Plugins** → **Installed** tab
|
||||
- Look for the Cline icon in your IDE's tool window bar (usually on the right side)
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Plugin appears to be installed but doesn't work:**
|
||||
- Ensure you've restarted your IDE after installation
|
||||
- Check if there are any error messages in the IDE's event log
|
||||
- Try disabling and re-enabling the plugin in Settings
|
||||
|
||||
**Performance or compatibility issues:**
|
||||
- Make sure you're using a supported JetBrains IDE version
|
||||
- Check for IDE updates that might improve compatibility
|
||||
- Consider allocating more memory to your IDE if needed
|
||||
|
||||
Having other issues? Join our [Discord community](https://discord.gg/cline) for help from the team and other users.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have Cline installed, you might want to:
|
||||
- Learn about [model selection](/getting-started/model-selection-guide) to choose the best AI provider
|
||||
- Explore [@ mentions](/features/at-mentions/overview) to reference files and context efficiently
|
||||
- Set up [Cline rules](/features/cline-rules) for your specific workflow
|
||||
- Try [MCP servers](/mcp/mcp-overview) to extend Cline's capabilities
|
||||
@@ -1,81 +1,254 @@
|
||||
---
|
||||
title: "Installing Cline"
|
||||
description: "Cline brings AI-powered coding assistance to your editor. Available for VS Code and JetBrains IDEs."
|
||||
description: "Get Cline set up in your editor and start building projects with AI assistance."
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing Cline, make sure you have the following:
|
||||
|
||||
### Create a Cline Account
|
||||
|
||||
Create a Cline account for the best experience. Creating a Cline account is completely free and you can [sign up here](https://app.cline.bot/signup). A Cline account provides:
|
||||
- Access to multiple AI models including stealth models
|
||||
- Seamless setup without needing to manage API keys
|
||||
- At times, we partner with model providers to offer inferencing at no cost through your Cline account
|
||||
|
||||
### Compatible Editor
|
||||
|
||||
Cline works with the following IDEs:
|
||||
- **VS Code** - Microsoft's popular code editor
|
||||
- **Cursor** - AI-powered code editor based on VS Code
|
||||
- **JetBrains IDEs** - IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and other JetBrains products
|
||||
- **VSCodium** - Open-source version of VS Code
|
||||
- **Windsurf** - VS Code-compatible editor
|
||||
|
||||
Make sure you have one of these editors installed before proceeding with the Cline installation.
|
||||
|
||||
## Choose Your Editor
|
||||
|
||||
Cline works across multiple development environments:
|
||||
Cline works across multiple IDEs. Select your preferred editor below for installation instructions:
|
||||
|
||||
- **VS Code/Cursor:** Install from VS Code Marketplace (most popular)
|
||||
- **JetBrains IDEs:** Install from JetBrains Marketplace - works in IntelliJ IDEA, PyCharm, WebStorm, and more
|
||||
- **VSCodium/Windsurf:** Install from Open VSX Registry
|
||||
<Tabs>
|
||||
<Tab title="VS Code/Cursor" icon="code">
|
||||
### Installation Steps
|
||||
|
||||
## VS Code Installation
|
||||
1. **Open VS Code** and navigate to the Extensions view (`Ctrl/Cmd + Shift + X`)
|
||||
2. **Search for "Cline"** in the Extensions marketplace
|
||||
3. **Click Install** on the Cline extension
|
||||
|
||||
### VS Code Marketplace: Step-by-Step Setup
|
||||
<Frame caption="VS Code marketplace with Cline extension ready to install">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
|
||||
alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Follow these steps to get Cline up and running:
|
||||
4. **Access Cline** after installation:
|
||||
- Click the Cline icon in the Activity Bar, or
|
||||
- Use Command Palette (`Ctrl/Cmd + Shift + P`) → "Cline: Open In New Tab"
|
||||
|
||||
1. **Open VS Code:** Launch the VS Code application.
|
||||
> **Note:** If VS Code shows "Running extensions might..." dialog, click "Allow". If you don't see the Cline icon, restart VS Code.
|
||||
|
||||
> **Note:** If VS Code shows "Running extensions might...", click "Allow".
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
2. **Open Your Cline Folder:** In VS Code, open the Cline folder you created in Documents.
|
||||
3. **Navigate to Extensions:** Click on the Extensions icon in the Activity Bar on the side of VS Code (`Ctrl + Shift + X` or `Cmd + Shift + X`).
|
||||
4. **Search for 'Cline':** In the Extensions search bar, type `Cline`.
|
||||
**Plugin Installation Issues**
|
||||
|
||||
<Frame caption="VS Code marketplace with Cline extension ready to install">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
|
||||
alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
</Frame>
|
||||
If you can't find Cline in the marketplace:
|
||||
- Make sure you're searching in the **Marketplace** tab (not Installed)
|
||||
- Try searching for "Cline AI" or just "Cline"
|
||||
- Check that your VS Code version is compatible
|
||||
|
||||
1. **Install the Extension:** Click the "Install" button next to the Cline extension.
|
||||
2. **Open Cline:**
|
||||
- Click the Cline icon in the Activity Bar.
|
||||
- Or, use the command palette (`Ctrl/Cmd + Shift + P`) and type "Cline: Open In New Tab" for a better view.
|
||||
3. **Troubleshooting:** If you don't see the Cline icon, try restarting VS Code.
|
||||
If installation fails:
|
||||
- Restart your VS Code and try again
|
||||
- Check your internet connection
|
||||
- Try installing from VSIX file as an alternative
|
||||
|
||||
> **Pro Tip:** You should see the Cline chat window appear in your VS Code editor!
|
||||
**Plugin Not Appearing**
|
||||
|
||||
### Open VSX Registry
|
||||
If you don't see the Cline tool window after installation:
|
||||
- Restart VS Code completely (File → Exit and reopen)
|
||||
- Check **View** → **Command Palette** → "Cline: Open In New Tab"
|
||||
- Verify the plugin is enabled in **Extensions** view
|
||||
- Look for the Cline icon in your Activity Bar (usually on the left side)
|
||||
|
||||
For VS Code-compatible editors without Marketplace access (like VSCodium and Windsurf):
|
||||
**Common Issues**
|
||||
|
||||
1. Open your editor.
|
||||
2. Access the Extensions view.
|
||||
3. Search for "Cline".
|
||||
4. Select "Cline" by saoudrizwan and click **Install**.
|
||||
5. Reload if prompted.
|
||||
Plugin appears to be installed but doesn't work:
|
||||
- Ensure you've restarted VS Code after installation
|
||||
- Check if there are any error messages in the Developer Console
|
||||
- Try disabling and re-enabling the extension
|
||||
|
||||
## JetBrains Installation
|
||||
Performance or compatibility issues:
|
||||
- Make sure you're using a supported VS Code version
|
||||
- Check for VS Code updates that might improve compatibility
|
||||
- Consider closing other resource-intensive extensions if needed
|
||||
|
||||
For IntelliJ IDEA, PyCharm, WebStorm, DataSpell, and other JetBrains IDEs:
|
||||
Need help? Join our [Discord community](https://discord.gg/cline).
|
||||
|
||||
1. Open your JetBrains IDE
|
||||
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS) to open Settings
|
||||
3. Go to Plugins → Marketplace tab
|
||||
4. Search "Cline" and click Install
|
||||
5. Restart your IDE
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="JetBrains IDEs" icon="brain">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-logo.svg"
|
||||
alt="JetBrains logo"
|
||||
style={{ width: "200px", height: "auto", margin: "0 auto 20px auto", display: "block" }}
|
||||
/>
|
||||
|
||||
**Need more help?** See our [complete JetBrains installation guide](/getting-started/installing-cline-jetbrains) for screenshots and troubleshooting.
|
||||
Cline for JetBrains works almost identically to Cline in VSCode. All the core features work properly: diff editing, using tools, logging in with different providers, MCP servers, Cline rules and workflows, and more.
|
||||
|
||||
### Creating Your Cline Account
|
||||
|
||||
Now that you have Cline installed, let's get you set up with your account:
|
||||
### Installation Steps
|
||||
|
||||
**Method 1: From IDE (Recommended)**
|
||||
1. Open your JetBrains IDE
|
||||
2. Go to **Settings** (`Ctrl+Alt+S` on Windows/Linux, `Cmd+,` on macOS)
|
||||
3. Navigate to **Plugins** → **Marketplace**
|
||||
4. Search for "Cline" and click **Install**
|
||||
5. Restart your IDE
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
**Method 2: Browser Install**
|
||||
|
||||
Visit the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline) and click **Install to IDE**.
|
||||
|
||||
<Accordion title="Method 3: Manual Installation">
|
||||
|
||||
1. Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline)
|
||||
2. Go to **Settings** → **Plugins**
|
||||
3. Click the gear icon → **Install Plugin from Disk**
|
||||
4. Select the downloaded `.zip` file
|
||||
5. Restart your IDE
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Using the Plugin
|
||||
|
||||
After installation, you’ll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to View → Tool Windows → Cline.
|
||||
|
||||
### Key Features
|
||||
|
||||
Cline for JetBrains includes all core features:
|
||||
- Diff editing and file modifications
|
||||
- Multiple API providers (Anthropic, OpenAI, local models)
|
||||
- MCP servers and custom tools
|
||||
- Cline rules and workflows
|
||||
- @ mentions for files, folders, and problems
|
||||
- Drag & drop support
|
||||
|
||||
> **Note:** Terminal output appears in collapsible sections rather than streaming directly to chat.
|
||||
|
||||
### Key Differences from VSCode
|
||||
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the Command Output section to see results.
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
**Plugin Installation Issues**
|
||||
|
||||
If you can't find Cline in the marketplace:
|
||||
- Make sure you're searching in the **Marketplace** tab (not Installed)
|
||||
- Try searching for "Cline AI" or just "Cline"
|
||||
- Check that your IDE version is compatible (2023.1 or later recommended)
|
||||
|
||||
If installation fails:
|
||||
- Restart your IDE and try again
|
||||
- Check your internet connection
|
||||
- Try installing from disk as an alternative
|
||||
|
||||
**Plugin Not Appearing**
|
||||
|
||||
If you don't see the Cline tool window after installation:
|
||||
- Restart your IDE completely (File → Exit and reopen)
|
||||
- Check **View** → **Tool Windows** → **Cline**
|
||||
- Verify the plugin is enabled in **Settings** → **Plugins** → **Installed** tab
|
||||
- Look for the Cline icon in your IDE's tool window bar (usually on the right side)
|
||||
|
||||
**Common Issues**
|
||||
|
||||
Plugin appears to be installed but doesn't work:
|
||||
- Ensure you've restarted your IDE after installation
|
||||
- Check if there are any error messages in the IDE's event log
|
||||
- Try disabling and re-enabling the plugin in Settings
|
||||
|
||||
Performance or compatibility issues:
|
||||
- Make sure you're using a supported JetBrains IDE version
|
||||
- Check for IDE updates that might improve compatibility
|
||||
- Consider allocating more memory to your IDE if needed
|
||||
|
||||
Need help? Join our [Discord community](https://discord.gg/cline).
|
||||
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="VSCodium/Windsurf" icon="terminal">
|
||||
### Installation Steps
|
||||
|
||||
For VS Code-compatible editors using Open VSX Registry:
|
||||
|
||||
1. **Open your editor** (VSCodium, Windsurf, etc.)
|
||||
2. **Navigate to Extensions view** (`Ctrl/Cmd + Shift + X`)
|
||||
3. **Search for "Cline"** in the marketplace
|
||||
4. **Select "Cline" by saoudrizwan** and click **Install**
|
||||
5. **Reload** if prompted
|
||||
|
||||
> **Note:** These editors use the Open VSX Registry instead of the VS Code Marketplace.
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
**Plugin Installation Issues**
|
||||
|
||||
If you can't find Cline in the marketplace:
|
||||
- Make sure you're searching in the **Marketplace** tab (not Installed)
|
||||
- Try searching for "Cline AI" or just "Cline"
|
||||
- Check that your editor version is compatible
|
||||
|
||||
If installation fails:
|
||||
- Restart your editor and try again
|
||||
- Check your internet connection
|
||||
- Try installing from disk as an alternative
|
||||
|
||||
**Plugin Not Appearing**
|
||||
|
||||
If you don't see the Cline tool window after installation:
|
||||
- Restart your editor completely (File → Exit and reopen)
|
||||
- Check **View** → **Command Palette** → "Cline: Open In New Tab"
|
||||
- Verify the plugin is enabled in **Extensions** view
|
||||
- Look for the Cline icon in your Activity Bar (usually on the left side)
|
||||
|
||||
**Common Issues**
|
||||
|
||||
Plugin appears to be installed but doesn't work:
|
||||
- Ensure you've restarted your editor after installation
|
||||
- Check if there are any error messages in the Developer Console
|
||||
- Try disabling and re-enabling the extension
|
||||
|
||||
Performance or compatibility issues:
|
||||
- Make sure you're using a supported editor version
|
||||
- Check for editor updates that might improve compatibility
|
||||
- Consider closing other resource-intensive extensions if needed
|
||||
|
||||
Need help? Join our [Discord community](https://discord.gg/cline).
|
||||
|
||||
</Accordion>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Sign In to Your Cline Account
|
||||
|
||||
Now that you have Cline installed, sign in to access your account:
|
||||
|
||||
1. **Open Cline** in your editor (click the Cline icon in the Activity Bar or Tool Windows)
|
||||
2. **Click "Sign In"** - you'll see this button in the Cline interface
|
||||
3. **Complete authentication** - you'll be redirected to [app.cline.bot](https://app.cline.bot) to sign in
|
||||
4. **Return to your editor** - once signed in, you'll be automatically redirected back
|
||||
|
||||
1. **Sign In to Cline:**
|
||||
- Click the **Sign In** button in the Cline extension.
|
||||
- You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account.
|
||||
2. **Start with Free Credits:**
|
||||
- No credit card needed!
|
||||
3. **Available AI Models:**
|
||||
- Anthropic Claude 3.5-Sonnet (recommended for coding)
|
||||
- DeepSeek Chat (cost-effective alternative)
|
||||
- Google Gemini 2.0 Flash
|
||||
- And more — all through your Cline account.
|
||||
|
||||
### Your First Interaction with Cline
|
||||
|
||||
@@ -96,4 +269,4 @@ Hey Cline! Could you help me create a new project folder called "hello-world" in
|
||||
|
||||
### Still Struggling?
|
||||
|
||||
Join our Discord community and engage with our team and other Cline users directly.
|
||||
Join our [Discord community](https://discord.gg/cline) and engage with our team and other Cline users directly.
|
||||
|
||||
@@ -9,7 +9,7 @@ New models drop constantly, so this guide focuses on what's working well with Cl
|
||||
|
||||
| Model | Context Window | Input Price* | Output Price* | Best For |
|
||||
|-------|---------------|--------------|---------------|----------|
|
||||
| **Claude Sonnet 4** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
|
||||
| **Claude Sonnet 4.5** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
|
||||
| **Qwen3 Coder** | 256K tokens | $0.20 | $0.80 | Coding tasks, open source flexibility |
|
||||
| **Gemini 2.5 Pro** | 1M+ tokens | TBD | TBD | Large codebases, document analysis |
|
||||
| **GPT-5** | 400K tokens | $1.25 | $10 | Latest OpenAI tech, three modes |
|
||||
@@ -57,9 +57,9 @@ New models drop constantly, so this guide focuses on what's working well with Cl
|
||||
|
||||
| If you want... | Use this |
|
||||
|----------------|----------|
|
||||
| Something that just works | Claude Sonnet 4 |
|
||||
| Something that just works | Claude Sonnet 4.5 |
|
||||
| To save money | DeepSeek V3 or Qwen3 variants |
|
||||
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4 |
|
||||
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4.5 |
|
||||
| Open source | Qwen3 Coder, Z AI GLM 4.5, or Kimi K2 |
|
||||
| Latest tech | GPT-5 |
|
||||
| Speed | Qwen3 Coder on Cerebras (fastest available) |
|
||||
@@ -74,6 +74,6 @@ Cline automatically handles context limits with [auto-compact](/features/auto-co
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
Start with **Claude Sonnet 4** if you want reliability. Experiment with **open source options** once you're comfortable to find the best fit for your workflow and budget.
|
||||
Start with **Claude Sonnet 4.5** if you want reliability. Experiment with **open source options** once you're comfortable to find the best fit for your workflow and budget.
|
||||
|
||||
The landscape moves fast - these recommendations reflect what's working now, but keep an eye on new releases.
|
||||
|
||||
@@ -53,7 +53,7 @@ Think of context like a whiteboard you and Cline share:
|
||||
- **Context Window** is the size of the whiteboard itself:
|
||||
- Measured in tokens (1 token ≈ 3/4 of an English word)
|
||||
- Each model has a fixed size:
|
||||
- Claude Sonnet 4: 1,000,000 tokens
|
||||
- Claude Sonnet 4.5: 1,000,000 tokens
|
||||
- Qwen3 Coder: 256,000 tokens
|
||||
- Gemini 2.5 Pro: 1,000,000+ tokens
|
||||
- GPT-5: 400,000 tokens
|
||||
@@ -77,7 +77,7 @@ Cline provides a visual way to monitor your context window usage through a progr
|
||||
- ↑ shows input tokens (what you've sent to the LLM)
|
||||
- ↓ shows output tokens (what the LLM has generated)
|
||||
- The progress bar visualizes how much of your context window you've used
|
||||
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4)
|
||||
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4.5)
|
||||
|
||||
### When to Watch the Bar
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "What is Cline?"
|
||||
description: "An introduction to Cline, your AI-powered development assistant in VS Code."
|
||||
description: "An introduction to Cline, your AI-powered development assistant for modern IDEs."
|
||||
---
|
||||
|
||||
Cline is an open source AI coding agent that brings frontier AI models directly to your VS Code editor. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
|
||||
Cline is an open source AI coding agent that brings frontier AI models directly to your IDE. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
|
||||
|
||||
## Open Source AI Coding, Uncompromised
|
||||
|
||||
@@ -69,4 +69,4 @@ Define project-specific instructions that Cline follows including coding standar
|
||||
|
||||
## Getting Started
|
||||
|
||||
Ready to experience AI coding without limits? [Install Cline](/getting-started/installing-cline) and start with our [Model Selection Guide](/getting-started/model-selection-guide) to choose the right AI model for your needs.
|
||||
Ready to experience AI coding without limits? [Install Cline](/getting-started/installing-cline) for your preferred IDE and start with our [Model Selection Guide](/getting-started/model-selection-guide) to choose the right AI model for your needs.
|
||||
|
||||
@@ -18,11 +18,8 @@ Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-sonnet-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `anthropic/claude-sonnet-4.5` (Recommended)
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-7-sonnet-20250219:thinking` (Extended Thinking variant)
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
- `claude-3-5-haiku-20241022`
|
||||
- `claude-3-opus-20240229`
|
||||
@@ -47,8 +44,8 @@ Cline users can leverage this by checking the `Enable Extended Thinking` box bel
|
||||
|
||||
**Key Aspects of Extended Thinking:**
|
||||
|
||||
- **Supported Models:** This feature is available for select models, including variants of Claude Opus 4, Claude Sonnet 4, and Claude Sonnet 3.7. The specific models listed in the "Supported Models" section above with the `:thinking` suffix are pre-configured in Cline to utilize this.
|
||||
- **Summarized Thinking (Claude 4):** For Claude 4 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
|
||||
- **Supported Models:** This feature is available for select models, including Claude Opus 4, Claude Sonnet 4.5, and Claude Sonnet 3.7.
|
||||
- **Summarized Thinking (Claude 4):** For Claude 4 and 4.5 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
|
||||
- **Streaming:** Extended thinking responses, including the `thinking` blocks, can be streamed.
|
||||
- **Tool Use & Prompt Caching:** Extended thinking interacts with tool use (requiring thinking blocks to be passed back) and prompt caching (with specific behaviors around cache invalidation and context).
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Custom styles for Cline documentation */
|
||||
|
||||
/* Make h1 titles lighter in font weight */
|
||||
h1 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* Also apply to any h1 elements within content areas */
|
||||
.content h1,
|
||||
.markdown h1,
|
||||
article h1,
|
||||
main h1 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* JetBrains logo visibility fix for dark mode */
|
||||
/* Add a subtle background and border to ensure visibility in both light and dark modes */
|
||||
img[alt="JetBrains logo"] {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode specific styling */
|
||||
[data-theme="dark"] img[alt="JetBrains logo"],
|
||||
.dark img[alt="JetBrains logo"] {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Media query for system dark mode preference */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
img[alt="JetBrains logo"] {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover effect for better interactivity */
|
||||
img[alt="JetBrains logo"]:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@@ -148,11 +148,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
|
||||
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
|
||||
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
|
||||
},
|
||||
{
|
||||
key: "alt+shift+c",
|
||||
command: "cline.openInNewTab",
|
||||
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
|
||||
},
|
||||
]
|
||||
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
|
||||
console.log(`Created keybindings.json to help with Cline activation`)
|
||||
@@ -187,11 +182,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
|
||||
setTimeout(() => {
|
||||
// Try to open Cline in the sidebar
|
||||
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
|
||||
|
||||
// Also try to open Cline in a tab as a fallback
|
||||
setTimeout(() => {
|
||||
require('vscode').commands.executeCommand('cline.openInNewTab');
|
||||
}, 5000);
|
||||
}, 5000);
|
||||
`
|
||||
fs.writeFileSync(startupScriptPath, startupScript)
|
||||
@@ -288,13 +278,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
|
||||
// Wait a moment for the sidebar to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Also open Cline in a tab as a fallback
|
||||
console.log('Opening Cline in a tab...');
|
||||
await vscode.commands.executeCommand('cline.openInNewTab');
|
||||
|
||||
// Wait a moment for the tab to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Create the test server if it doesn't exist
|
||||
console.log('Creating test server...');
|
||||
|
||||
|
||||
Generated
+1554
-5
File diff suppressed because it is too large
Load Diff
+10
-51
@@ -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.32.1",
|
||||
"version": "3.32.6",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -133,11 +133,6 @@
|
||||
"title": "History",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "cline.popoutButtonClicked",
|
||||
"title": "Open in Editor",
|
||||
"icon": "$(link-external)"
|
||||
},
|
||||
{
|
||||
"command": "cline.accountButtonClicked",
|
||||
"title": "Account",
|
||||
@@ -148,11 +143,6 @@
|
||||
"title": "Settings",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "cline.openInNewTab",
|
||||
"title": "Open In New Tab",
|
||||
"category": "Cline"
|
||||
},
|
||||
{
|
||||
"command": "cline.dev.createTestTasks",
|
||||
"title": "Create Test Tasks",
|
||||
@@ -178,7 +168,10 @@
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"title": "Generate Commit Message with Cline",
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
"icon": {
|
||||
"light": "assets/icons/robot_panel_light.png",
|
||||
"dark": "assets/icons/robot_panel_dark.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
@@ -246,11 +239,6 @@
|
||||
"group": "navigation@3",
|
||||
"when": "view == claude-dev.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.popoutButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "view == claude-dev.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.accountButtonClicked",
|
||||
"group": "navigation@5",
|
||||
@@ -262,38 +250,6 @@
|
||||
"when": "view == claude-dev.SidebarProvider"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "cline.plusButtonClicked",
|
||||
"group": "navigation@1",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.mcpButtonClicked",
|
||||
"group": "navigation@2",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.historyButtonClicked",
|
||||
"group": "navigation@3",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.popoutButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.accountButtonClicked",
|
||||
"group": "navigation@5",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "cline.settingsButtonClicked",
|
||||
"group": "navigation@6",
|
||||
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "cline.addToChat",
|
||||
@@ -356,11 +312,12 @@
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
|
||||
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
|
||||
"ci:check-all": "npm-run-all -p check-types lint format",
|
||||
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test:ci": "node scripts/test-ci.js",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
@@ -412,12 +369,14 @@
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "5.6.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^17.1.0",
|
||||
"prebuild-install": "^7.1.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"proxyquire": "^2.1.3",
|
||||
|
||||
@@ -19,6 +19,7 @@ service StateService {
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
|
||||
rpc updateModelBannerVersion(Int64Request) returns (Empty);
|
||||
}
|
||||
message DictationSettings {
|
||||
bool feature_enabled = 1;
|
||||
|
||||
@@ -2,9 +2,33 @@ syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "cline/state.proto";
|
||||
import "cline/models.proto";
|
||||
import "cline/browser.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message AutoApprovalActions {
|
||||
bool read_files = 1;
|
||||
bool read_files_externally = 2;
|
||||
bool edit_files = 3;
|
||||
bool edit_files_externally = 4;
|
||||
bool execute_safe_commands = 5;
|
||||
bool execute_all_commands = 6;
|
||||
bool use_browser = 7;
|
||||
bool use_mcp = 8;
|
||||
}
|
||||
|
||||
// Auto approval settings for task execution
|
||||
message AutoApprovalSettings {
|
||||
int32 version = 1;
|
||||
bool enabled = 2;
|
||||
AutoApprovalActions actions = 3;
|
||||
int32 max_requests = 4;
|
||||
bool enable_notifications = 5;
|
||||
repeated string favorites = 6;
|
||||
}
|
||||
|
||||
service TaskService {
|
||||
// Cancels the currently running task
|
||||
rpc cancelTask(EmptyRequest) returns (Empty);
|
||||
@@ -36,12 +60,138 @@ service TaskService {
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
}
|
||||
|
||||
// Task-specific settings
|
||||
message TaskSettings {
|
||||
string aws_region = 1;
|
||||
bool aws_use_cross_region_inference = 2;
|
||||
bool aws_bedrock_use_prompt_cache = 3;
|
||||
string aws_bedrock_endpoint = 4;
|
||||
string aws_profile = 5;
|
||||
string aws_authentication = 6;
|
||||
bool aws_use_profile = 7;
|
||||
string vertex_project_id = 8;
|
||||
string vertex_region = 9;
|
||||
string requesty_base_url = 10;
|
||||
string open_ai_base_url = 11;
|
||||
map<string, string> open_ai_headers = 12;
|
||||
string ollama_base_url = 13;
|
||||
string ollama_api_options_ctx_num = 14;
|
||||
string lm_studio_base_url = 15;
|
||||
string lm_studio_max_tokens = 16;
|
||||
string anthropic_base_url = 17;
|
||||
string gemini_base_url = 18;
|
||||
string azure_api_version = 19;
|
||||
string open_router_provider_sorting = 20;
|
||||
AutoApprovalSettings auto_approval_settings = 21;
|
||||
BrowserSettings browser_settings = 24;
|
||||
string lite_llm_base_url = 25;
|
||||
bool lite_llm_use_prompt_cache = 26;
|
||||
int32 fireworks_model_max_completion_tokens = 27;
|
||||
int32 fireworks_model_max_tokens = 28;
|
||||
string qwen_api_line = 29;
|
||||
string moonshot_api_line = 30;
|
||||
string zai_api_line = 31;
|
||||
string telemetry_setting = 32;
|
||||
string asksage_api_url = 33;
|
||||
bool plan_act_separate_models_setting = 34;
|
||||
bool enable_checkpoints_setting = 35;
|
||||
int32 request_timeout_ms = 36;
|
||||
int32 shell_integration_timeout = 37;
|
||||
string default_terminal_profile = 38;
|
||||
int32 terminal_output_line_limit = 39;
|
||||
string sap_ai_core_token_url = 40;
|
||||
string sap_ai_core_base_url = 41;
|
||||
string sap_ai_resource_group = 42;
|
||||
bool sap_ai_core_use_orchestration_mode = 43;
|
||||
string claude_code_path = 44;
|
||||
string qwen_code_oauth_path = 45;
|
||||
bool strict_plan_mode_enabled = 46;
|
||||
bool yolo_mode_toggled = 47;
|
||||
bool use_auto_condense = 48;
|
||||
string preferred_language = 49;
|
||||
OpenaiReasoningEffort openai_reasoning_effort = 50;
|
||||
PlanActMode mode = 51;
|
||||
DictationSettings dictation_settings = 52;
|
||||
FocusChainSettings focus_chain_settings = 53;
|
||||
string custom_prompt = 54;
|
||||
string dify_base_url = 55;
|
||||
double auto_condense_threshold = 56;
|
||||
string oca_base_url = 57;
|
||||
ApiProvider plan_mode_api_provider = 58;
|
||||
string plan_mode_api_model_id = 59;
|
||||
int64 plan_mode_thinking_budget_tokens = 60;
|
||||
string plan_mode_reasoning_effort = 61;
|
||||
LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
|
||||
bool plan_mode_aws_bedrock_custom_selected = 63;
|
||||
string plan_mode_aws_bedrock_custom_model_base_id = 64;
|
||||
string plan_mode_open_router_model_id = 65;
|
||||
OpenRouterModelInfo plan_mode_open_router_model_info = 66;
|
||||
string plan_mode_open_ai_model_id = 67;
|
||||
OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
|
||||
string plan_mode_ollama_model_id = 69;
|
||||
string plan_mode_lm_studio_model_id = 70;
|
||||
string plan_mode_lite_llm_model_id = 71;
|
||||
LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
|
||||
string plan_mode_requesty_model_id = 73;
|
||||
OpenRouterModelInfo plan_mode_requesty_model_info = 74;
|
||||
string plan_mode_together_model_id = 75;
|
||||
string plan_mode_fireworks_model_id = 76;
|
||||
string plan_mode_sap_ai_core_model_id = 77;
|
||||
string plan_mode_sap_ai_core_deployment_id = 78;
|
||||
string plan_mode_groq_model_id = 79;
|
||||
OpenRouterModelInfo plan_mode_groq_model_info = 80;
|
||||
string plan_mode_baseten_model_id = 81;
|
||||
OpenRouterModelInfo plan_mode_baseten_model_info = 82;
|
||||
string plan_mode_hugging_face_model_id = 83;
|
||||
OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
|
||||
string plan_mode_huawei_cloud_maas_model_id = 85;
|
||||
OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
|
||||
string plan_mode_oca_model_id = 87;
|
||||
OcaModelInfo plan_mode_oca_model_info = 88;
|
||||
ApiProvider act_mode_api_provider = 89;
|
||||
string act_mode_api_model_id = 90;
|
||||
int64 act_mode_thinking_budget_tokens = 91;
|
||||
string act_mode_reasoning_effort = 92;
|
||||
LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
|
||||
bool act_mode_aws_bedrock_custom_selected = 94;
|
||||
string act_mode_aws_bedrock_custom_model_base_id = 95;
|
||||
string act_mode_open_router_model_id = 96;
|
||||
OpenRouterModelInfo act_mode_open_router_model_info = 97;
|
||||
string act_mode_open_ai_model_id = 98;
|
||||
OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
|
||||
string act_mode_ollama_model_id = 100;
|
||||
string act_mode_lm_studio_model_id = 101;
|
||||
string act_mode_lite_llm_model_id = 102;
|
||||
LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
|
||||
string act_mode_requesty_model_id = 104;
|
||||
OpenRouterModelInfo act_mode_requesty_model_info = 105;
|
||||
string act_mode_together_model_id = 106;
|
||||
string act_mode_fireworks_model_id = 107;
|
||||
string act_mode_sap_ai_core_model_id = 108;
|
||||
string act_mode_sap_ai_core_deployment_id = 109;
|
||||
string act_mode_groq_model_id = 110;
|
||||
OpenRouterModelInfo act_mode_groq_model_info = 111;
|
||||
string act_mode_baseten_model_id = 112;
|
||||
OpenRouterModelInfo act_mode_baseten_model_info = 113;
|
||||
string act_mode_hugging_face_model_id = 114;
|
||||
OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
|
||||
string act_mode_huawei_cloud_maas_model_id = 116;
|
||||
OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
|
||||
string plan_mode_vercel_ai_gateway_model_id = 118;
|
||||
OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
|
||||
string act_mode_vercel_ai_gateway_model_id = 120;
|
||||
OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
|
||||
string act_mode_oca_model_id = 122;
|
||||
OcaModelInfo act_mode_oca_model_info = 123;
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
message NewTaskRequest {
|
||||
Metadata metadata = 1;
|
||||
string text = 2;
|
||||
repeated string images = 3;
|
||||
repeated string files = 4;
|
||||
optional TaskSettings task_settings = 5;
|
||||
}
|
||||
|
||||
// Request message for toggling task favorite status
|
||||
|
||||
+6
-18
@@ -5,18 +5,6 @@ import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Enum for webview provider types
|
||||
enum WebviewProviderType {
|
||||
SIDEBAR = 0;
|
||||
TAB = 1;
|
||||
}
|
||||
|
||||
// Define a new message type for webview provider info
|
||||
message WebviewProviderTypeRequest {
|
||||
Metadata metadata = 1;
|
||||
WebviewProviderType provider_type = 2;
|
||||
}
|
||||
|
||||
// Enum for ClineMessage type
|
||||
enum ClineMessageType {
|
||||
ASK = 0;
|
||||
@@ -229,13 +217,13 @@ service UiService {
|
||||
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
|
||||
|
||||
// Subscribe to addToInput events (when user adds content via context menu)
|
||||
rpc subscribeToAddToInput(StringRequest) returns (stream String);
|
||||
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
|
||||
|
||||
// Subscribe to MCP button clicked events
|
||||
rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
rpc subscribeToMcpButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to history button click events
|
||||
rpc subscribeToHistoryButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
rpc subscribeToHistoryButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
|
||||
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
@@ -244,7 +232,7 @@ service UiService {
|
||||
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to settings button clicked events
|
||||
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
rpc subscribeToSettingsButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to partial message updates (streaming Cline messages as they're built)
|
||||
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
|
||||
@@ -255,8 +243,8 @@ service UiService {
|
||||
// Subscribe to relinquish control events
|
||||
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to focus chat input events with client ID
|
||||
rpc subscribeToFocusChatInput(StringRequest) returns (stream Empty);
|
||||
// Subscribe to focus chat input events
|
||||
rpc subscribeToFocusChatInput(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to webview visibility change events
|
||||
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
|
||||
|
||||
@@ -34,10 +34,16 @@ service EnvService {
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
// The name of the host platform, e.g VSCode
|
||||
// The name of the host platform, e.g VSCode, IntelliJ Ultimate Edition, etc.
|
||||
optional string platform = 1;
|
||||
// The version of the host platform, e.g. 1.103.0
|
||||
// The version of the host platform, e.g. 1.103.0 for VSCode, or 2025.1.1.1 for JetBrains IDEs.
|
||||
optional string version = 2;
|
||||
// The type of the cline host environment, e.g. 'VSCode Extension', 'Cline for JetBrains', 'CLI'
|
||||
// This is different from the platform because there are many JetBrains IDEs, but they all use the same
|
||||
// plugin.
|
||||
optional string cline_type = 3;
|
||||
// The version of the cline host environment, e.g. 33.2.10 for extension, or 1.0.6 for JetBrains.
|
||||
optional string cline_version = 4;
|
||||
}
|
||||
|
||||
enum Setting {
|
||||
|
||||
@@ -27,6 +27,9 @@ service WorkspaceService {
|
||||
|
||||
// Opens and focuses the Cline sidebar panel in the host IDE.
|
||||
rpc openClineSidebarPanel(OpenClineSidebarPanelRequest) returns (OpenClineSidebarPanelResponse);
|
||||
|
||||
// Opens and focuses the terminal panel.
|
||||
rpc openTerminalPanel(OpenTerminalRequest) returns (OpenTerminalResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -82,12 +85,11 @@ message SearchWorkspaceItemsResponse {
|
||||
|
||||
message OpenProblemsPanelRequest {}
|
||||
message OpenProblemsPanelResponse {}
|
||||
|
||||
message OpenInFileExplorerPanelRequest {
|
||||
string path = 1;
|
||||
}
|
||||
message OpenInFileExplorerPanelResponse {}
|
||||
|
||||
// Request/response for opening the Cline sidebar
|
||||
message OpenClineSidebarPanelRequest {}
|
||||
message OpenClineSidebarPanelResponse {}
|
||||
message OpenTerminalRequest {}
|
||||
message OpenTerminalResponse {}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const process = require("process")
|
||||
|
||||
try {
|
||||
if (process.platform === "linux") {
|
||||
console.log("Detected Linux environment.")
|
||||
|
||||
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" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.platform === "linux") {
|
||||
console.error(
|
||||
`Error: xvfb-run is not installed.\n` +
|
||||
`Please install it using the following command:\n` +
|
||||
` Debian/Ubuntu: sudo apt install xvfb\n` +
|
||||
` RHEL/CentOS: sudo yum install xvfb\n` +
|
||||
` Arch Linux: sudo pacman -S xvfb`,
|
||||
)
|
||||
} else {
|
||||
console.error("Error running tests:", error.message)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
+2
-3
@@ -7,7 +7,6 @@ import {
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
@@ -53,13 +52,13 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
const sidebarWebview = HostProvider.get().createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
const webview = HostProvider.get().createWebviewProvider()
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
return sidebarWebview
|
||||
return webview
|
||||
}
|
||||
|
||||
async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("ClaudeCodeHandler", () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
handler = new ClaudeCodeHandler({
|
||||
claudeCodePath: "/mock/path",
|
||||
apiModelId: "claude-3-5-sonnet-20241022",
|
||||
apiModelId: "claude-opus-4-1-20250805",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -229,11 +229,11 @@ describe("ClaudeCodeHandler", () => {
|
||||
describe("getModel", () => {
|
||||
it("should return the correct model when specified", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-3-5-sonnet-20241022",
|
||||
apiModelId: "claude-sonnet-4-5-20250929",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("claude-3-5-sonnet-20241022")
|
||||
model.id.should.equal("claude-sonnet-4-5-20250929")
|
||||
})
|
||||
|
||||
it("should return default model when not specified", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -44,16 +44,18 @@ export class AnthropicHandler implements ApiHandler {
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
|
||||
const modelId = model.id.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
? model.id.slice(0, -CLAUDE_SONNET_4_1M_SUFFIX.length)
|
||||
: model.id
|
||||
const enable1mContextWindow = model.id.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
const modelId = model.id.endsWith(CLAUDE_SONNET_1M_SUFFIX) ? model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length) : model.id
|
||||
const enable1mContextWindow = model.id.endsWith(CLAUDE_SONNET_1M_SUFFIX)
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = !!((modelId.includes("3-7") || modelId.includes("4-")) && budget_tokens !== 0)
|
||||
const reasoningOn = !!(
|
||||
(modelId.includes("3-7") || modelId.includes("4-") || modelId.includes("4-5")) &&
|
||||
budget_tokens !== 0
|
||||
)
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-sonnet-4-5-20250929":
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
InvokeModelWithResponseStreamCommand,
|
||||
} from "@aws-sdk/client-bedrock-runtime"
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -119,11 +119,11 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
// cross region inference requires prefixing the model id with the region
|
||||
const rawModelId = await this.getModelId()
|
||||
|
||||
const modelId = rawModelId.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
? rawModelId.slice(0, -CLAUDE_SONNET_4_1M_SUFFIX.length)
|
||||
const modelId = rawModelId.endsWith(CLAUDE_SONNET_1M_SUFFIX)
|
||||
? rawModelId.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
: rawModelId
|
||||
|
||||
const enable1mContextWindow = rawModelId.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
const enable1mContextWindow = rawModelId.endsWith(CLAUDE_SONNET_1M_SUFFIX)
|
||||
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -741,7 +741,10 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
*/
|
||||
private shouldEnableReasoning(baseModelId: string, budgetTokens: number): boolean {
|
||||
return (
|
||||
(baseModelId.includes("3-7") || baseModelId.includes("sonnet-4") || baseModelId.includes("opus-4")) &&
|
||||
(baseModelId.includes("3-7") ||
|
||||
baseModelId.includes("sonnet-4") ||
|
||||
baseModelId.includes("opus-4") ||
|
||||
baseModelId.includes("sonnet-4-5")) &&
|
||||
budgetTokens !== 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import OpenAI from "openai"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -46,15 +46,17 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
if (!this.client) {
|
||||
try {
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": this.options.ulid || "",
|
||||
}
|
||||
Object.assign(defaultHeaders, await buildClineExtraHeaders())
|
||||
|
||||
this.client = new OpenAI({
|
||||
baseURL: `${this._baseUrl}/api/v1`,
|
||||
apiKey: clineAccountAuthToken,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": this.options.ulid || "",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
defaultHeaders,
|
||||
// Capture real HTTP request ID from initial streaming response headers
|
||||
fetch: async (...args: Parameters<typeof fetch>): Promise<Awaited<ReturnType<typeof fetch>>> => {
|
||||
const [input, init] = args
|
||||
@@ -204,11 +206,14 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
// Align with backend auth expectations
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
}
|
||||
Object.assign(headers, await buildClineExtraHeaders())
|
||||
|
||||
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
|
||||
headers: {
|
||||
// Align with backend auth expectations
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
},
|
||||
headers,
|
||||
timeout: 15_000, // this request hangs sometimes
|
||||
})
|
||||
|
||||
|
||||
+40
-19
@@ -1,8 +1,13 @@
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import { withRetry } from "./retry"
|
||||
|
||||
describe("Retry Decorator", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe("withRetry", () => {
|
||||
it("should not retry on success", async () => {
|
||||
let callCount = 0
|
||||
@@ -73,9 +78,11 @@ describe("Retry Decorator", () => {
|
||||
|
||||
it("should respect retry-after header with delta seconds", async () => {
|
||||
let callCount = 0
|
||||
const startTime = Date.now()
|
||||
const setTimeoutSpy = sinon.spy(global, "setTimeout")
|
||||
const baseDelay = 1000
|
||||
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
|
||||
@withRetry({ maxRetries: 2, baseDelay }) // Use large baseDelay to ensure header takes precedence
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -94,19 +101,23 @@ describe("Retry Decorator", () => {
|
||||
result.push(value)
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
duration.should.be.approximately(10, 10) // Allow 10ms variance
|
||||
callCount.should.equal(2)
|
||||
setTimeoutSpy.calledOnce.should.be.true
|
||||
const [_, delay] = setTimeoutSpy.getCall(0).args
|
||||
delay?.should.equal(0)
|
||||
|
||||
result.should.deepEqual(["success after retry"])
|
||||
})
|
||||
|
||||
it("should respect retry-after header with Unix timestamp", async () => {
|
||||
const setTimeoutSpy = sinon.spy(global, "setTimeout")
|
||||
let callCount = 0
|
||||
const startTime = Date.now()
|
||||
const retryTimestamp = Math.floor(Date.now() / 1000) + 0.01 // 10ms in the future
|
||||
const fixedDate = new Date("2010-01-01T00:00:00.000Z")
|
||||
const retryTimestamp = Math.floor(fixedDate.getTime() / 1000) + 0.01 // 10ms in the future
|
||||
const baseDelay = 1000
|
||||
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
|
||||
@withRetry({ maxRetries: 2, baseDelay }) // Use large baseDelay to ensure header takes precedence
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -125,17 +136,22 @@ describe("Retry Decorator", () => {
|
||||
result.push(value)
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
duration.should.be.approximately(10, 10) // Allow 10ms variance
|
||||
callCount.should.equal(2)
|
||||
|
||||
setTimeoutSpy.calledOnce.should.be.true
|
||||
const [_, delay] = setTimeoutSpy.getCall(0).args
|
||||
delay?.should.equal(fixedDate.getTime())
|
||||
|
||||
result.should.deepEqual(["success after retry"])
|
||||
})
|
||||
|
||||
it("should use exponential backoff when no retry-after header", async () => {
|
||||
const setTimeoutSpy = sinon.spy(global, "setTimeout")
|
||||
let callCount = 0
|
||||
const startTime = Date.now()
|
||||
const baseDelay = 10
|
||||
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
|
||||
@withRetry({ maxRetries: 2, baseDelay, maxDelay: 100 })
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -153,18 +169,22 @@ describe("Retry Decorator", () => {
|
||||
result.push(value)
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
// First retry should be after baseDelay (10ms)
|
||||
duration.should.be.approximately(10, 10)
|
||||
callCount.should.equal(2)
|
||||
setTimeoutSpy.calledOnce.should.be.true
|
||||
const [_, delay] = setTimeoutSpy.getCall(0).args
|
||||
delay?.should.equal(baseDelay)
|
||||
|
||||
result.should.deepEqual(["success after retry"])
|
||||
})
|
||||
|
||||
it("should respect maxDelay", async () => {
|
||||
const setTimeoutSpy = sinon.spy(global, "setTimeout")
|
||||
let callCount = 0
|
||||
const startTime = Date.now()
|
||||
const baseDelay = 50
|
||||
const maxDelay = 10
|
||||
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 3, baseDelay: 50, maxDelay: 10 })
|
||||
@withRetry({ maxRetries: 3, baseDelay, maxDelay })
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount < 3) {
|
||||
@@ -182,10 +202,11 @@ describe("Retry Decorator", () => {
|
||||
result.push(value)
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
// Both retries should be capped at maxDelay (10ms each)
|
||||
duration.should.be.approximately(20, 20)
|
||||
callCount.should.equal(3)
|
||||
setTimeoutSpy.calledOnce.should.be.true
|
||||
const [_, delay] = setTimeoutSpy.getCall(0).args
|
||||
delay?.should.equal(maxDelay)
|
||||
|
||||
result.should.deepEqual(["success after retries"])
|
||||
})
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export function convertToOpenAiMessages(
|
||||
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
|
||||
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
|
||||
// @ts-ignore-next-line
|
||||
reasoning_details: reasoningDetails,
|
||||
reasoning_details: reasoningDetails.length > 0 ? reasoningDetails : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo, openRouterClaudeSonnet41mModelId } from "@shared/api"
|
||||
import {
|
||||
CLAUDE_SONNET_1M_SUFFIX,
|
||||
ModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
@@ -19,16 +24,18 @@ export async function createOpenRouterStream(
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet41m = model.id === openRouterClaudeSonnet41mModelId
|
||||
if (isClaudeSonnet41m) {
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_4_1M_SUFFIX.length)
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// handles direct model.id match logic
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet": // OpenRouter accidentally included this in model list for a brief moment, and users may be using this model id. And to support prompt caching, we need to add it here.
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
@@ -88,6 +95,8 @@ export async function createOpenRouterStream(
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
@@ -124,6 +133,8 @@ export async function createOpenRouterStream(
|
||||
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
@@ -169,7 +180,7 @@ export async function createOpenRouterStream(
|
||||
? { provider: { order: ["groq", "together", "baseten", "parasail", "novita", "deepinfra"], allow_fallbacks: false } }
|
||||
: {}),
|
||||
// limit providers to only those that support the 1m context window
|
||||
...(isClaudeSonnet41m ? { provider: { order: ["anthropic", "amazon-bedrock"], allow_fallbacks: false } } : {}),
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function discoverBrowser(controller: Controller, _request: EmptyReq
|
||||
// This way we don't override the user's preference
|
||||
|
||||
// Test the connection to get the endpoint
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
return BrowserConnection.create({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return ChromePath.create({
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
|
||||
// Relaunch Chrome in debug mode
|
||||
await browserSession.relaunchChromeDebugMode(controller)
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const text = request.value || ""
|
||||
|
||||
// If no text is provided, try auto-discovery
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { Controller } from "../index"
|
||||
import { sendAddToInputEventToClient } from "../ui/subscribeToAddToInput"
|
||||
import { sendAddToInputEvent } from "../ui/subscribeToAddToInput"
|
||||
|
||||
// 'Add to Cline' context menu in editor and code action
|
||||
// Inserts the selected code into the chat.
|
||||
@@ -22,10 +21,7 @@ export async function addToCline(controller: Controller, request: CommandContext
|
||||
input += `\nProblems:\n${problemsString}`
|
||||
}
|
||||
|
||||
const lastActiveWebview = WebviewProvider.getLastActiveInstance()
|
||||
if (lastActiveWebview) {
|
||||
await sendAddToInputEventToClient(lastActiveWebview.getClientId(), input)
|
||||
}
|
||||
await sendAddToInputEvent(input)
|
||||
|
||||
console.log("addToCline", request.selectedText, filePath, request.language)
|
||||
telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
|
||||
|
||||
@@ -31,9 +31,7 @@ async function handleInstallWithCline(
|
||||
* Handles copying the installation command to clipboard
|
||||
*/
|
||||
async function handleCopyCommand(installCommand: string): Promise<void> {
|
||||
const vscode = await import("vscode")
|
||||
await vscode.env.clipboard.writeText(installCommand)
|
||||
|
||||
await HostProvider.env.clipboardWriteText({ value: installCommand })
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Installation command copied to clipboard: ${installCommand}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { searchWorkspaceFiles } from "@services/search/file-search"
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
|
||||
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
@@ -16,6 +17,10 @@ export async function searchFiles(_controller: Controller, request: FileSearchRe
|
||||
if (!workspacePath) {
|
||||
// Handle case where workspace path is not available
|
||||
console.error("Error in searchFiles: No workspace path available")
|
||||
|
||||
// Track as a specific failure type - no workspace available
|
||||
await telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
|
||||
|
||||
return { results: [], mentionsRequestId: request.mentionsRequestId }
|
||||
}
|
||||
|
||||
@@ -39,12 +44,42 @@ export async function searchFiles(_controller: Controller, request: FileSearchRe
|
||||
// Convert search results to proto FileInfo objects using the conversion function
|
||||
const protoResults = convertSearchResultsToProtoFileInfos(searchResults)
|
||||
|
||||
// Track search results telemetry
|
||||
// Determine search type for telemetry
|
||||
let searchType: "file" | "folder" | "all" = "all"
|
||||
if (request.selectedType === FileSearchType.FILE) {
|
||||
searchType = "file"
|
||||
} else if (request.selectedType === FileSearchType.FOLDER) {
|
||||
searchType = "folder"
|
||||
}
|
||||
|
||||
await telemetryService.captureMentionSearchResults(
|
||||
request.query || "",
|
||||
protoResults.length,
|
||||
searchType,
|
||||
protoResults.length === 0,
|
||||
)
|
||||
|
||||
// Return successful results
|
||||
return { results: protoResults, mentionsRequestId: request.mentionsRequestId }
|
||||
} catch (error) {
|
||||
// Log the error but don't include it in the response, following the pattern in searchCommits
|
||||
console.error("Error in searchFiles:", error)
|
||||
|
||||
// Track as a search execution error with appropriate error type
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorType = error instanceof Error && error.message.includes("permission") ? "permission_denied" : "unknown"
|
||||
|
||||
// Determine mention type based on the search request
|
||||
const mentionType =
|
||||
request.selectedType === FileSearchType.FILE
|
||||
? "file"
|
||||
: request.selectedType === FileSearchType.FOLDER
|
||||
? "folder"
|
||||
: "folder" // Default to folder for "all" searches
|
||||
|
||||
await telemetryService.captureMentionFailed(mentionType, errorType, errorMessage)
|
||||
|
||||
// Return empty results without error message
|
||||
return { results: [], mentionsRequestId: request.mentionsRequestId }
|
||||
}
|
||||
|
||||
@@ -40,10 +40,12 @@ import {
|
||||
GlobalFileNames,
|
||||
} from "../storage/disk"
|
||||
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Settings } from "../storage/state-keys"
|
||||
import { Task } from "../task"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -52,7 +54,6 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
readonly id: string
|
||||
task?: Task
|
||||
|
||||
mcpHub: McpHub
|
||||
@@ -64,11 +65,7 @@ export class Controller {
|
||||
// NEW: Add workspace manager (optional initially)
|
||||
private workspaceManager?: WorkspaceRootManager
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
id: string,
|
||||
) {
|
||||
this.id = id
|
||||
constructor(readonly context: vscode.ExtensionContext) {
|
||||
PromptRegistry.getInstance() // Ensure prompts and tools are registered
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.stateManager = new StateManager(context)
|
||||
@@ -118,21 +115,17 @@ export class Controller {
|
||||
|
||||
this.mcpHub = new McpHub(
|
||||
() => ensureMcpServersDirectoryExists(),
|
||||
() => ensureSettingsDirectoryExists(this.context),
|
||||
() => ensureSettingsDirectoryExists(),
|
||||
ExtensionRegistryInfo.version,
|
||||
telemetryService,
|
||||
)
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
|
||||
cleanupLegacyCheckpoints().catch((error) => {
|
||||
console.error("Failed to cleanup legacy checkpoints:", error)
|
||||
})
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<Mode> {
|
||||
return this.stateManager.getGlobalSettingsKey("mode")
|
||||
}
|
||||
|
||||
/*
|
||||
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
|
||||
- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
|
||||
@@ -195,7 +188,13 @@ export class Controller {
|
||||
this.stateManager.setGlobalState("userInfo", info)
|
||||
}
|
||||
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
async initTask(
|
||||
task?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
historyItem?: HistoryItem,
|
||||
taskSettings?: Partial<Settings>,
|
||||
) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
@@ -230,30 +229,33 @@ export class Controller {
|
||||
|
||||
const cwd = this.workspaceManager?.getPrimaryRoot()?.path || (await getCwd(getDesktopDir()))
|
||||
|
||||
this.task = new Task(
|
||||
this,
|
||||
this.mcpHub,
|
||||
(historyItem) => this.updateTaskHistory(historyItem),
|
||||
() => this.postStateToWebview(),
|
||||
(taskId) => this.reinitExistingTaskFromId(taskId),
|
||||
() => this.cancelTask(),
|
||||
const taskId = historyItem?.id || Date.now().toString()
|
||||
|
||||
await this.stateManager.loadTaskSettings(taskId)
|
||||
if (taskSettings) {
|
||||
this.stateManager.setTaskSettingsBatch(taskId, taskSettings)
|
||||
}
|
||||
|
||||
this.task = new Task({
|
||||
controller: this,
|
||||
mcpHub: this.mcpHub,
|
||||
updateTaskHistory: (historyItem) => this.updateTaskHistory(historyItem),
|
||||
postStateToWebview: () => this.postStateToWebview(),
|
||||
reinitExistingTaskFromId: (taskId) => this.reinitExistingTaskFromId(taskId),
|
||||
cancelTask: () => this.cancelTask(),
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile ?? "default",
|
||||
terminalReuseEnabled: terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
cwd,
|
||||
this.stateManager,
|
||||
this.workspaceManager,
|
||||
stateManager: this.stateManager,
|
||||
workspaceManager: this.workspaceManager,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
historyItem,
|
||||
)
|
||||
|
||||
// Load task settings after task creation
|
||||
if (this.task.taskId) {
|
||||
await this.stateManager.loadTaskSettings(this.task.taskId)
|
||||
}
|
||||
taskId,
|
||||
})
|
||||
}
|
||||
|
||||
async reinitExistingTaskFromId(taskId: string) {
|
||||
@@ -368,7 +370,7 @@ export class Controller {
|
||||
// Get current settings to determine how to update providers
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await this.getCurrentMode()
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
@@ -419,7 +421,7 @@ export class Controller {
|
||||
// Get current settings to determine how to update providers
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await this.getCurrentMode()
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
@@ -461,6 +463,11 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async handleTaskCreation(prompt: string) {
|
||||
await sendChatButtonClickedEvent()
|
||||
await this.initTask(prompt)
|
||||
}
|
||||
|
||||
// MCP Marketplace
|
||||
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
|
||||
try {
|
||||
@@ -576,7 +583,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
const openrouter: ApiProvider = "openrouter"
|
||||
const currentMode = await this.getCurrentMode()
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
@@ -680,7 +687,7 @@ export class Controller {
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
await sendStateUpdate(this.id, state)
|
||||
await sendStateUpdate(state)
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -718,6 +725,7 @@ export class Controller {
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
|
||||
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
@@ -798,13 +806,14 @@ export class Controller {
|
||||
featureFlag: featureFlagsService.getMultiRootEnabled(),
|
||||
},
|
||||
lastDismissedInfoBannerVersion,
|
||||
lastDismissedModelBannerVersion,
|
||||
}
|
||||
}
|
||||
|
||||
async clearTask() {
|
||||
if (this.task) {
|
||||
// Clear task settings cache when task ends
|
||||
await this.stateManager.clearTaskSettings(this.task.taskId)
|
||||
await this.stateManager.clearTaskSettings()
|
||||
}
|
||||
await this.task?.abortTask()
|
||||
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
|
||||
@@ -73,7 +73,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
// Initialize task and show chat view
|
||||
await controller.initTask(task)
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
await sendChatButtonClickedEvent()
|
||||
|
||||
// Return the download details directly
|
||||
return McpDownloadResponse.create({
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
|
||||
// Which mode(s) to update?
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = (await controller.getCurrentMode?.()) ?? "plan"
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
const planModeSelectedModelId =
|
||||
apiConfiguration?.planModeOcaModelId && models[apiConfiguration.planModeOcaModelId]
|
||||
? apiConfiguration.planModeOcaModelId
|
||||
|
||||
@@ -5,7 +5,12 @@ import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { CLAUDE_SONNET_4_1M_TIERS, clineCodeSupernovaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
|
||||
import {
|
||||
CLAUDE_SONNET_1M_TIERS,
|
||||
clineCodeSupernovaModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@/shared/api"
|
||||
import { Controller } from ".."
|
||||
|
||||
type OpenRouterSupportedParams =
|
||||
@@ -108,6 +113,8 @@ export async function refreshOpenRouterModels(
|
||||
})
|
||||
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
// NOTE: we artificially restrict the context window to 200k to keep costs low for users, and have a :1m model variant created below for users that want to use the full 1m.
|
||||
modelInfo.contextWindow = 200_000
|
||||
@@ -213,11 +220,14 @@ export async function refreshOpenRouterModels(
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
// add custom :1m model variant
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4") {
|
||||
const claudeSonnet41mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet41mModelInfo.contextWindow = 1_000_000 // limiting providers to those that support 1m context window
|
||||
claudeSonnet41mModelInfo.tiers = CLAUDE_SONNET_4_1M_TIERS
|
||||
models[openRouterClaudeSonnet41mModelId] = claudeSonnet41mModelInfo
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4" || rawModel.id === "anthropic/claude-sonnet-4.5") {
|
||||
const claudeSonnet1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet1mModelInfo.contextWindow = 1_000_000 // limiting providers to those that support 1m context window
|
||||
claudeSonnet1mModelInfo.tiers = CLAUDE_SONNET_1M_TIERS
|
||||
// sonnet 4
|
||||
models[openRouterClaudeSonnet41mModelId] = claudeSonnet1mModelInfo
|
||||
// sonnet 4.5
|
||||
models[openRouterClaudeSonnet451mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,11 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
|
||||
const baseUrl = controller.stateManager.getGlobalSettingsKey("requestyBaseUrl")
|
||||
|
||||
const resolvedUrl = toRequestyServiceUrl(baseUrl)
|
||||
const url = new URL(`${resolvedUrl.pathname}/models`, resolvedUrl).toString()
|
||||
const url = resolvedUrl != null ? new URL(`${resolvedUrl.pathname}/models`, resolvedUrl).toString() : undefined
|
||||
|
||||
if (url == null) {
|
||||
throw new Error("URL is not valid.")
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function updateApiConfigurationProto(
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
controller.task.api = buildApiHandler({ ...appApiConfiguration, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
})
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
await sendChatButtonClickedEvent()
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ExtensionState } from "@/shared/ExtensionMessage"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active state subscriptions by controller ID
|
||||
const activeStateSubscriptions = new Map<string, StreamingResponseHandler<State>>()
|
||||
// Keep track of active state subscriptions
|
||||
const activeStateSubscriptions = new Set<StreamingResponseHandler<State>>()
|
||||
|
||||
/**
|
||||
* Subscribe to state updates
|
||||
@@ -20,59 +20,61 @@ export async function subscribeToState(
|
||||
responseStream: StreamingResponseHandler<State>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
|
||||
// Send the initial state
|
||||
const initialState = await controller.getStateToPostToWebview()
|
||||
const initialStateJson = JSON.stringify(initialState)
|
||||
|
||||
//console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
|
||||
|
||||
await responseStream({
|
||||
stateJson: initialStateJson,
|
||||
})
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeStateSubscriptions.set(controllerId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeStateSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeStateSubscriptions.delete(controllerId)
|
||||
//console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
|
||||
activeStateSubscriptions.delete(responseStream)
|
||||
//console.log(`[DEBUG] Cleaned up state subscription`)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "state_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a state update to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the state to
|
||||
* @param state The state to send
|
||||
*/
|
||||
export async function sendStateUpdate(controllerId: string, state: ExtensionState): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeStateSubscriptions.get(controllerId)
|
||||
// Send the initial state
|
||||
const initialState = await controller.getStateToPostToWebview()
|
||||
const initialStateJson = JSON.stringify(initialState)
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active state subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
//console.log(`[DEBUG] set up state subscription`)
|
||||
|
||||
try {
|
||||
const stateJson = JSON.stringify(state)
|
||||
await responseStream(
|
||||
{
|
||||
stateJson,
|
||||
stateJson: initialStateJson,
|
||||
},
|
||||
false, // Not the last message
|
||||
)
|
||||
//console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
|
||||
} catch (error) {
|
||||
console.error(`Error sending state update to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeStateSubscriptions.delete(controllerId)
|
||||
console.error("Error sending initial state:", error)
|
||||
activeStateSubscriptions.delete(responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a state update to all active subscribers
|
||||
* @param state The state to send
|
||||
*/
|
||||
export async function sendStateUpdate(state: ExtensionState): Promise<void> {
|
||||
// Send the state to all active subscribers
|
||||
const promises = Array.from(activeStateSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const stateJson = JSON.stringify(state)
|
||||
await responseStream(
|
||||
{
|
||||
stateJson,
|
||||
},
|
||||
false, // Not the last message
|
||||
)
|
||||
//console.log(`[DEBUG] sending followup state`, stateJson.length, "chars")
|
||||
} catch (error) {
|
||||
console.error("Error sending state update:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeStateSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Empty, Int64Request } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Updates the model banner version to track which version the user has dismissed
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the version number
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function updateModelBannerVersion(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
const version = Number(request.value)
|
||||
|
||||
controller.stateManager.setGlobalState("lastDismissedModelBannerVersion", version)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setApiConfiguration(convertedApiConfigurationFromProto)
|
||||
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfigForHandler = {
|
||||
...convertedApiConfigurationFromProto,
|
||||
ulid: controller.task.ulid,
|
||||
|
||||
@@ -1,14 +1,75 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { PlanActMode, OpenaiReasoningEffort as ProtoOpenaiReasoningEffort } from "@shared/proto/cline/state"
|
||||
import { NewTaskRequest } from "@shared/proto/cline/task"
|
||||
import { Settings } from "@/core/storage/state-keys"
|
||||
import { convertProtoToApiProvider } from "@/shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
|
||||
import { convertProtoToAutoApprovalSettings } from "../../../shared/proto-conversions/models/auto-approval-settings-conversion"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Creates a new task with the given text and optional images
|
||||
* @param controller The controller instance
|
||||
* @param request The new task request containing text and optional images
|
||||
* @param request The new task request containing text and optional images, and optional task settings
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function newTask(controller: Controller, request: NewTaskRequest): Promise<Empty> {
|
||||
await controller.initTask(request.text, request.images, request.files)
|
||||
const convertOpenaiReasoningEffort = (effort: ProtoOpenaiReasoningEffort): string => {
|
||||
switch (effort) {
|
||||
case ProtoOpenaiReasoningEffort.LOW:
|
||||
return "low"
|
||||
case ProtoOpenaiReasoningEffort.MEDIUM:
|
||||
return "medium"
|
||||
case ProtoOpenaiReasoningEffort.HIGH:
|
||||
return "high"
|
||||
case ProtoOpenaiReasoningEffort.MINIMAL:
|
||||
return "minimal"
|
||||
default:
|
||||
return "medium"
|
||||
}
|
||||
}
|
||||
|
||||
const convertPlanActMode = (mode: PlanActMode): string => {
|
||||
return mode === PlanActMode.PLAN ? "plan" : "act"
|
||||
}
|
||||
|
||||
const filteredTaskSettings: Partial<Settings> = Object.fromEntries(
|
||||
Object.entries({
|
||||
...request.taskSettings,
|
||||
...(request.taskSettings?.autoApprovalSettings && {
|
||||
autoApprovalSettings: convertProtoToAutoApprovalSettings({
|
||||
...request.taskSettings.autoApprovalSettings,
|
||||
metadata: {},
|
||||
}),
|
||||
}),
|
||||
...(request.taskSettings?.browserSettings && {
|
||||
browserSettings: {
|
||||
viewport: request.taskSettings.browserSettings.viewport || DEFAULT_BROWSER_SETTINGS.viewport,
|
||||
remoteBrowserHost: request.taskSettings.browserSettings.remoteBrowserHost,
|
||||
remoteBrowserEnabled: request.taskSettings.browserSettings.remoteBrowserEnabled,
|
||||
chromeExecutablePath: request.taskSettings.browserSettings.chromeExecutablePath,
|
||||
disableToolUse: request.taskSettings.browserSettings.disableToolUse,
|
||||
customArgs: request.taskSettings.browserSettings.customArgs,
|
||||
},
|
||||
}),
|
||||
...(request.taskSettings?.openaiReasoningEffort !== undefined && {
|
||||
openaiReasoningEffort: convertOpenaiReasoningEffort(request.taskSettings.openaiReasoningEffort),
|
||||
}),
|
||||
...(request.taskSettings?.mode !== undefined && {
|
||||
mode: convertPlanActMode(request.taskSettings.mode),
|
||||
}),
|
||||
...(request.taskSettings?.customPrompt === "compact" && {
|
||||
customPrompt: "compact",
|
||||
}),
|
||||
...(request.taskSettings?.planModeApiProvider !== undefined && {
|
||||
planModeApiProvider: convertProtoToApiProvider(request.taskSettings.planModeApiProvider),
|
||||
}),
|
||||
...(request.taskSettings?.actModeApiProvider !== undefined && {
|
||||
actModeApiProvider: convertProtoToApiProvider(request.taskSettings.actModeApiProvider),
|
||||
}),
|
||||
}).filter(([_, value]) => value !== undefined),
|
||||
)
|
||||
|
||||
await controller.initTask(request.text, request.images, request.files, undefined, filteredTaskSettings)
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
|
||||
await controller.initTask(undefined, undefined, undefined, historyItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
await sendChatButtonClickedEvent()
|
||||
|
||||
// Return task data for gRPC response
|
||||
return TaskResponse.create({
|
||||
@@ -47,7 +47,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
|
||||
await controller.initTask(undefined, undefined, undefined, fetchedItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
await sendChatButtonClickedEvent()
|
||||
|
||||
return TaskResponse.create({
|
||||
id: fetchedItem.id,
|
||||
|
||||
@@ -9,9 +9,6 @@ import type { Controller } from "../index"
|
||||
* resolved through `resolveWebviewView()`.
|
||||
*/
|
||||
export async function getWebviewHtml(_controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
const webviewProvider = WebviewProvider.getLastActiveInstance()
|
||||
if (!webviewProvider) {
|
||||
throw new Error("No active webview")
|
||||
}
|
||||
return Promise.resolve(String.create({ value: webviewProvider.getHtmlContent() }))
|
||||
const webview = WebviewProvider.getInstance()
|
||||
return Promise.resolve(String.create({ value: webview.getHtmlContent() }))
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
@@ -76,7 +76,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
// Update model info in state for Groq (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
@@ -123,7 +123,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
@@ -164,7 +164,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
// Update model info in state for Vercel AI Gateway (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Track subscriptions by controller ID
|
||||
const activeSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
// Keep track of active account button clicked subscriptions
|
||||
const activeAccountButtonClickedSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to account button clicked events
|
||||
@@ -13,43 +13,45 @@ const activeSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
* @param requestId The request ID for cleanup
|
||||
*/
|
||||
export async function subscribeToAccountButtonClicked(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up accountButtonClicked subscription`)
|
||||
|
||||
// Store subscription with controller ID
|
||||
activeSubscriptions.set(controllerId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeAccountButtonClickedSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeSubscriptions.delete(controllerId)
|
||||
activeAccountButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "account_button_subscription" }, responseStream)
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "accountButtonClicked_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send account button clicked event to a specific controller
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
* Send an account button clicked event to all active subscribers
|
||||
*/
|
||||
export async function sendAccountButtonClickedEvent(controllerId: string): Promise<void> {
|
||||
const responseStream = activeSubscriptions.get(controllerId)
|
||||
export async function sendAccountButtonClickedEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeAccountButtonClickedSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending accountButtonClicked event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeAccountButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: Empty = Empty.create({})
|
||||
await responseStream(event, false)
|
||||
} catch (error) {
|
||||
console.error(`Error sending account button clicked event to controller ${controllerId}:`, error)
|
||||
activeSubscriptions.delete(controllerId)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
import type { String as ProtoString, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { EmptyRequest, String as ProtoString } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, type StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active addToInput subscriptions
|
||||
const activeAddToInputSubscriptions = new Set<StreamingResponseHandler<ProtoString>>()
|
||||
|
||||
// Map client IDs to their subscription handlers for targeted sending
|
||||
const addToInputSubscriptions = new Map<string, StreamingResponseHandler<ProtoString>>()
|
||||
|
||||
/**
|
||||
* Subscribe to addToInput events
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the client ID
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToAddToInput(
|
||||
_controller: Controller,
|
||||
request: StringRequest,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<ProtoString>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const clientId = request.value
|
||||
if (!clientId) {
|
||||
throw new Error("Client ID is required for addToInput subscription")
|
||||
}
|
||||
console.log("[DEBUG] set up addToInput subscription")
|
||||
|
||||
console.log("[DEBUG] set up addToInput subscription for client:", clientId)
|
||||
|
||||
// Add this subscription to both the general set and the client-specific map
|
||||
// Add this subscription to the active subscriptions
|
||||
activeAddToInputSubscriptions.add(responseStream)
|
||||
addToInputSubscriptions.set(clientId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeAddToInputSubscriptions.delete(responseStream)
|
||||
addToInputSubscriptions.delete(clientId)
|
||||
console.log("[DEBUG] Cleaned up addToInput subscription for client:", clientId)
|
||||
console.log("[DEBUG] Cleaned up addToInput subscription")
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -70,33 +60,3 @@ export async function sendAddToInputEvent(text: string): Promise<void> {
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an addToInput event to a specific webview by client ID
|
||||
* @param clientId The ID of the client to send the event to
|
||||
* @param text The text to add to the input
|
||||
*/
|
||||
export async function sendAddToInputEventToClient(clientId: string, text: string): Promise<void> {
|
||||
const responseStream = addToInputSubscriptions.get(clientId)
|
||||
if (!responseStream) {
|
||||
console.warn(`No addToInput subscription found for client ID: ${clientId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: ProtoString = {
|
||||
value: text,
|
||||
}
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
console.log("[DEBUG] sending addToInput event to client", clientId, ":", text.length, "chars")
|
||||
} catch (error) {
|
||||
console.error(`Error sending addToInput event to client ${clientId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
addToInputSubscriptions.delete(clientId)
|
||||
// Also remove from the general set
|
||||
activeAddToInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active chatButtonClicked subscriptions by controller ID
|
||||
const activeChatButtonClickedSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
// Keep track of active chatButtonClicked subscriptions
|
||||
const activeChatButtonClickedSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to chatButtonClicked events
|
||||
@@ -13,20 +13,19 @@ const activeChatButtonClickedSubscriptions = new Map<string, StreamingResponseHa
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToChatButtonClicked(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up chatButtonClicked subscription for controller ${controllerId}`)
|
||||
console.log(`[DEBUG] set up chatButtonClicked subscription`)
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeChatButtonClickedSubscriptions.set(controllerId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeChatButtonClickedSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeChatButtonClickedSubscriptions.delete(controllerId)
|
||||
activeChatButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -36,27 +35,23 @@ export async function subscribeToChatButtonClicked(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chatButtonClicked event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
* Send a chatButtonClicked event to all active subscribers
|
||||
*/
|
||||
export async function sendChatButtonClickedEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeChatButtonClickedSubscriptions.get(controllerId)
|
||||
export async function sendChatButtonClickedEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeChatButtonClickedSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending chatButtonClicked event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeChatButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending chatButtonClicked event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeChatButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active didBecomeVisible subscriptions by controller ID
|
||||
const activeDidBecomeVisibleSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
// Keep track of active didBecomeVisible subscriptions
|
||||
const activeDidBecomeVisibleSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to didBecomeVisible events
|
||||
@@ -13,20 +13,19 @@ const activeDidBecomeVisibleSubscriptions = new Map<string, StreamingResponseHan
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToDidBecomeVisible(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up didBecomeVisible subscription for controller ${controllerId}`)
|
||||
console.log(`[DEBUG] set up didBecomeVisible subscription`)
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeDidBecomeVisibleSubscriptions.set(controllerId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeDidBecomeVisibleSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeDidBecomeVisibleSubscriptions.delete(controllerId)
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -36,27 +35,23 @@ export async function subscribeToDidBecomeVisible(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a didBecomeVisible event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
* Send a didBecomeVisible event to all active subscribers
|
||||
*/
|
||||
export async function sendDidBecomeVisibleEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeDidBecomeVisibleSubscriptions.get(controllerId)
|
||||
export async function sendDidBecomeVisibleEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeDidBecomeVisibleSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending didBecomeVisible event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: Empty = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending didBecomeVisible event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeDidBecomeVisibleSubscriptions.delete(controllerId)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Map client IDs to their subscription handlers
|
||||
const focusChatInputSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
// Keep track of active focus chat input subscriptions
|
||||
const focusChatInputSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to focus chat input events
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the client ID
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request
|
||||
*/
|
||||
export async function subscribeToFocusChatInput(
|
||||
_controller: Controller,
|
||||
request: StringRequest,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const clientId = request.value
|
||||
if (!clientId) {
|
||||
throw new Error("Client ID is required for focusChatInput subscription")
|
||||
}
|
||||
|
||||
// Store this subscription with its client ID
|
||||
focusChatInputSubscriptions.set(clientId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
focusChatInputSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
focusChatInputSubscriptions.delete(clientId)
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -38,25 +33,23 @@ export async function subscribeToFocusChatInput(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a focus chat input event to a specific webview by client ID
|
||||
* @param clientId The ID of the client to send the event to
|
||||
* Send a focus chat input event to all active subscribers
|
||||
*/
|
||||
export async function sendFocusChatInputEvent(clientId: string): Promise<void> {
|
||||
const responseStream = focusChatInputSubscriptions.get(clientId)
|
||||
if (!responseStream) {
|
||||
console.warn(`No subscription found for client ID: ${clientId}`)
|
||||
return
|
||||
}
|
||||
export async function sendFocusChatInputEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(focusChatInputSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending focus chat input event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending focus chat input event to client ${clientId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
focusChatInputSubscriptions.delete(clientId)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { WebviewProviderType, WebviewProviderTypeRequest } from "@shared/proto/cline/ui"
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active subscriptions with their provider type
|
||||
const activeHistoryButtonClickedSubscriptions = new Map<StreamingResponseHandler<Empty>, WebviewProviderType>()
|
||||
// Keep track of active history button clicked subscriptions
|
||||
const activeHistoryButtonClickedSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to history button clicked events
|
||||
* @param controller The controller instance
|
||||
* @param request The webview provider type request
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToHistoryButtonClicked(
|
||||
_controller: Controller,
|
||||
request: WebviewProviderTypeRequest,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Extract the provider type from the request
|
||||
const providerType = request.providerType
|
||||
console.log(`[DEBUG] set up history button subscription for ${WebviewProviderType[providerType]} webview`)
|
||||
|
||||
// Add this subscription to the active subscriptions with its provider type
|
||||
activeHistoryButtonClickedSubscriptions.set(responseStream, providerType)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeHistoryButtonClickedSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
@@ -39,16 +34,10 @@ export async function subscribeToHistoryButtonClicked(
|
||||
|
||||
/**
|
||||
* Send a history button clicked event to all active subscribers
|
||||
* @param webviewType Optional filter to send only to a specific webview type
|
||||
*/
|
||||
export async function sendHistoryButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
|
||||
// Send the event to all active subscribers matching the webview type (if specified)
|
||||
const promises = Array.from(activeHistoryButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
|
||||
// Skip subscribers of different types if webview type is specified
|
||||
if (webviewType !== undefined && webviewType !== providerType) {
|
||||
return
|
||||
}
|
||||
|
||||
export async function sendHistoryButtonClickedEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeHistoryButtonClickedSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
@@ -56,7 +45,7 @@ export async function sendHistoryButtonClickedEvent(webviewType?: WebviewProvide
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending history button clicked event to ${WebviewProviderType[providerType]}:`, error)
|
||||
console.error("Error sending history button clicked event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeHistoryButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active mcpButtonClicked subscriptions by controller ID
|
||||
const activeMcpButtonClickedSubscriptions = new Map<string, StreamingResponseHandler<Empty>>()
|
||||
// Keep track of active mcpButtonClicked subscriptions
|
||||
const activeMcpButtonClickedSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to mcpButtonClicked events
|
||||
@@ -13,20 +13,19 @@ const activeMcpButtonClickedSubscriptions = new Map<string, StreamingResponseHan
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToMcpButtonClicked(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up mcpButtonClicked subscription for controller ${controllerId}`)
|
||||
console.log(`[DEBUG] set up mcpButtonClicked subscription`)
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeMcpButtonClickedSubscriptions.set(controllerId, responseStream)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeMcpButtonClickedSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeMcpButtonClickedSubscriptions.delete(controllerId)
|
||||
activeMcpButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -36,27 +35,23 @@ export async function subscribeToMcpButtonClicked(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mcpButtonClicked event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
* Send a mcpButtonClicked event to all active subscribers
|
||||
*/
|
||||
export async function sendMcpButtonClickedEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeMcpButtonClickedSubscriptions.get(controllerId)
|
||||
export async function sendMcpButtonClickedEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeMcpButtonClickedSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending mcpButtonClicked event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeMcpButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
if (!responseStream) {
|
||||
console.error(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending mcpButtonClicked event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeMcpButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { WebviewProviderType, WebviewProviderTypeRequest } from "@shared/proto/cline/ui"
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Track subscriptions with their provider type
|
||||
const subscriptions = new Map<StreamingResponseHandler<Empty>, WebviewProviderType>()
|
||||
// Keep track of active settings button clicked subscriptions
|
||||
const activeSettingsButtonClickedSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to settings button clicked events
|
||||
* @param controller The controller instance
|
||||
* @param request The request with provider type
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToSettingsButtonClicked(
|
||||
_controller: Controller,
|
||||
request: WebviewProviderTypeRequest,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const providerType = request.providerType
|
||||
console.log(`[DEBUG] set up settings button subscription for ${WebviewProviderType[providerType]} webview`)
|
||||
|
||||
// Store the subscription with its provider type
|
||||
subscriptions.set(responseStream, providerType)
|
||||
// Add this subscription to the active subscriptions
|
||||
activeSettingsButtonClickedSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
subscriptions.delete(responseStream)
|
||||
activeSettingsButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -37,23 +33,17 @@ export async function subscribeToSettingsButtonClicked(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a settings button clicked event to active subscribers of matching provider type
|
||||
* @param webviewType The type of webview that triggered the event
|
||||
* Send a settings button clicked event to all active subscribers
|
||||
*/
|
||||
export async function sendSettingsButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
|
||||
// Process all subscriptions, filtering based on the source
|
||||
const promises = Array.from(subscriptions.entries()).map(async ([responseStream, providerType]) => {
|
||||
// If webviewType is provided, only send to subscribers of the same type
|
||||
if (webviewType !== undefined && webviewType !== providerType) {
|
||||
return // Skip subscribers of different types
|
||||
}
|
||||
|
||||
export async function sendSettingsButtonClickedEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeSettingsButtonClickedSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(event, false) // Not the last message
|
||||
} catch (error) {
|
||||
console.error(`Error sending settings button clicked event to ${WebviewProviderType[providerType]}:`, error)
|
||||
subscriptions.delete(responseStream)
|
||||
console.error("Error sending settings button clicked event:", error)
|
||||
activeSettingsButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import { extractTextFromFile } from "@integrations/misc/extract-text"
|
||||
import { openFile } from "@integrations/misc/open-file"
|
||||
import { getLatestTerminalOutput } from "@integrations/terminal/get-latest-output"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { mentionRegexGlobal } from "@shared/context-mentions"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { getCommitInfo, getWorkingState } from "@utils/git"
|
||||
import fs from "fs/promises"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { DiagnosticSeverity } from "@/shared/proto/index.cline"
|
||||
@@ -38,7 +38,7 @@ export async function openMention(mention?: string): Promise<void> {
|
||||
} else if (mention === "problems") {
|
||||
await HostProvider.workspace.openProblemsPanel({})
|
||||
} else if (mention === "terminal") {
|
||||
vscode.commands.executeCommand("workbench.action.terminal.focus")
|
||||
await HostProvider.workspace.openTerminalPanel({})
|
||||
} else if (mention.startsWith("http")) {
|
||||
await openExternal(mention)
|
||||
}
|
||||
@@ -111,21 +111,28 @@ export async function parseMentions(
|
||||
let result: string
|
||||
if (launchBrowserError) {
|
||||
result = `Error fetching content: ${launchBrowserError.message}`
|
||||
// Track failed URL mention
|
||||
telemetryService.captureMentionFailed("url", "network_error", launchBrowserError?.message || "")
|
||||
} else {
|
||||
try {
|
||||
const markdown = await urlContentFetcher.urlToMarkdown(mention)
|
||||
result = markdown
|
||||
// Track successful URL mention
|
||||
telemetryService.captureMentionUsed("url", markdown.length)
|
||||
} catch (error) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
})
|
||||
result = `Error fetching content: ${error.message}`
|
||||
// Track failed URL mention
|
||||
telemetryService.captureMentionFailed("url", "network_error", error.message)
|
||||
}
|
||||
}
|
||||
parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
|
||||
} else if (isFileMention(mention)) {
|
||||
const mentionPath = getFilePathFromMention(mention)
|
||||
const mentionType = mention.endsWith("/") ? "folder" : "file"
|
||||
try {
|
||||
const content = await getFileOrFolderContent(mentionPath, cwd)
|
||||
if (mention.endsWith("/")) {
|
||||
@@ -137,40 +144,67 @@ export async function parseMentions(
|
||||
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
|
||||
}
|
||||
}
|
||||
// Track successful file/folder mention
|
||||
telemetryService.captureMentionUsed(mentionType, content.length)
|
||||
} catch (error) {
|
||||
if (mention.endsWith("/")) {
|
||||
parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${error.message}\n</folder_content>`
|
||||
} else {
|
||||
parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${error.message}\n</file_content>`
|
||||
}
|
||||
// Track failed file/folder mention
|
||||
// Map file access errors to appropriate error types
|
||||
let errorType: "not_found" | "permission_denied" | "unknown" = "unknown"
|
||||
if (error.message.includes("ENOENT") || error.message.includes("Failed to access")) {
|
||||
errorType = "not_found"
|
||||
} else if (error.message.includes("EACCES") || error.message.includes("permission")) {
|
||||
errorType = "permission_denied"
|
||||
}
|
||||
telemetryService.captureMentionFailed(mentionType, errorType, error.message)
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
try {
|
||||
const problems = await getWorkspaceProblems()
|
||||
parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
|
||||
// Track successful problems mention
|
||||
telemetryService.captureMentionUsed("problems", problems.length)
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
// Track failed problems mention
|
||||
telemetryService.captureMentionFailed("problems", "unknown", error.message)
|
||||
}
|
||||
} else if (mention === "terminal") {
|
||||
try {
|
||||
const terminalOutput = await getLatestTerminalOutput()
|
||||
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
|
||||
// Track successful terminal mention
|
||||
telemetryService.captureMentionUsed("terminal", terminalOutput.length)
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
|
||||
// Track failed terminal mention
|
||||
telemetryService.captureMentionFailed("terminal", "unknown", error.message)
|
||||
}
|
||||
} else if (mention === "git-changes") {
|
||||
try {
|
||||
const workingState = await getWorkingState(cwd)
|
||||
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
|
||||
// Track successful git-changes mention
|
||||
telemetryService.captureMentionUsed("git-changes", workingState.length)
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
|
||||
// Track failed git-changes mention
|
||||
telemetryService.captureMentionFailed("git-changes", "unknown", error.message)
|
||||
}
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
try {
|
||||
const commitInfo = await getCommitInfo(mention, cwd)
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
|
||||
// Track successful commit mention
|
||||
telemetryService.captureMentionUsed("commit", commitInfo.length)
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
|
||||
// Track failed commit mention
|
||||
telemetryService.captureMentionFailed("commit", "unknown", error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,8 +697,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
+3
-2
@@ -555,8 +555,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
+3
-2
@@ -521,8 +521,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
+3
-2
@@ -496,8 +496,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
+3
-2
@@ -535,8 +535,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
@@ -537,8 +537,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
@@ -503,8 +503,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
+3
-2
@@ -480,8 +480,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
@@ -517,8 +517,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
@@ -70,8 +70,9 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
3. IMPORTANT: When you determine that you need to make several changes to the same file, prefer to use a single replace_in_file call with multiple SEARCH/REPLACE blocks. DO NOT prefer to make multiple successive replace_in_file calls for the same file. For example, if you were to add a component to a file, you would use a single replace_in_file call with a SEARCH/REPLACE block to add the import statement and another SEARCH/REPLACE block to add the component usage, rather than making one replace_in_file call for the import statement and then another separate replace_in_file call for the component usage.
|
||||
4. For major overhauls or initial file creation, rely on write_to_file.
|
||||
5. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.`
|
||||
|
||||
export async function getEditingFilesSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class StateManager {
|
||||
|
||||
// Debounced persistence state
|
||||
private pendingGlobalState = new Set<GlobalStateAndSettingsKey>()
|
||||
private pendingTaskState = new Set<SettingsKey>()
|
||||
private pendingTaskState = new Map<string, Set<SettingsKey>>()
|
||||
private pendingSecrets = new Set<SecretKey>()
|
||||
private pendingWorkspaceState = new Set<LocalStateKey>()
|
||||
private persistenceTimeout: NodeJS.Timeout | null = null
|
||||
@@ -132,8 +132,11 @@ export class StateManager {
|
||||
this.taskStateCache[key] = value
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingTaskState.add(key)
|
||||
this.scheduleDebouncedPersistence(taskId)
|
||||
if (!this.pendingTaskState.has(taskId)) {
|
||||
this.pendingTaskState.set(taskId, new Set())
|
||||
}
|
||||
this.pendingTaskState.get(taskId)!.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,12 +151,15 @@ export class StateManager {
|
||||
Object.assign(this.taskStateCache, updates)
|
||||
|
||||
// Then track the keys for persistence
|
||||
if (!this.pendingTaskState.has(taskId)) {
|
||||
this.pendingTaskState.set(taskId, new Set())
|
||||
}
|
||||
Object.keys(updates).forEach((key) => {
|
||||
this.pendingTaskState.add(key as SettingsKey)
|
||||
this.pendingTaskState.get(taskId)!.add(key as SettingsKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence(taskId)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,12 +188,12 @@ export class StateManager {
|
||||
/**
|
||||
* Clear task settings cache - ensures pending changes are persisted first
|
||||
*/
|
||||
async clearTaskSettings(taskId?: string): Promise<void> {
|
||||
async clearTaskSettings(): Promise<void> {
|
||||
// If there are pending task settings, persist them first
|
||||
if (this.pendingTaskState.size > 0 && taskId) {
|
||||
if (this.pendingTaskState.size > 0) {
|
||||
try {
|
||||
// Persist pending task state immediately
|
||||
await this.persistTaskStateBatch(this.pendingTaskState, taskId)
|
||||
await this.persistTaskStateBatch(this.pendingTaskState)
|
||||
// Clear pending set after successful persistence
|
||||
this.pendingTaskState.clear()
|
||||
} catch (error) {
|
||||
@@ -723,7 +729,7 @@ export class StateManager {
|
||||
/**
|
||||
* Schedule debounced persistence - simple timeout-based persistence
|
||||
*/
|
||||
private scheduleDebouncedPersistence(taskId?: string): void {
|
||||
private scheduleDebouncedPersistence(): void {
|
||||
// Clear existing timeout if one is pending
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
@@ -736,7 +742,7 @@ export class StateManager {
|
||||
this.persistGlobalStateBatch(this.pendingGlobalState),
|
||||
this.persistSecretsBatch(this.pendingSecrets),
|
||||
this.persistWorkspaceStateBatch(this.pendingWorkspaceState),
|
||||
this.persistTaskStateBatch(this.pendingTaskState, taskId),
|
||||
this.persistTaskStateBatch(this.pendingTaskState),
|
||||
])
|
||||
|
||||
// Clear pending sets on successful persistence
|
||||
@@ -776,16 +782,27 @@ export class StateManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist task state keys with Promise.all
|
||||
* Private method to batch persist task state keys with a single write operation
|
||||
*/
|
||||
private async persistTaskStateBatch(keys: Set<SettingsKey>, taskId: string | undefined): Promise<void> {
|
||||
if (!taskId) {
|
||||
private async persistTaskStateBatch(pendingTaskStates: Map<string, Set<SettingsKey>>): Promise<void> {
|
||||
if (pendingTaskStates.size === 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Persist each task's settings
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
return writeTaskSettingsToStorage(taskId, { [key]: this.taskStateCache[key] })
|
||||
Array.from(pendingTaskStates.entries()).map(([taskId, keys]) => {
|
||||
if (keys.size === 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const settingsToWrite: Record<string, any> = {}
|
||||
for (const key of keys) {
|
||||
const value = this.taskStateCache[key]
|
||||
if (value !== undefined) {
|
||||
settingsToWrite[key] = value
|
||||
}
|
||||
}
|
||||
return writeTaskSettingsToStorage(taskId, settingsToWrite)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
+14
-18
@@ -7,9 +7,8 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { GlobalState } from "./state-keys"
|
||||
import { GlobalState, Settings } from "./state-keys"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
@@ -65,9 +64,7 @@ export async function getDocumentsPath(): Promise<string> {
|
||||
}
|
||||
|
||||
export async function ensureTaskDirectoryExists(taskId: string): Promise<string> {
|
||||
const taskDir = path.join(HostProvider.get().globalStorageFsPath, "tasks", taskId)
|
||||
await fs.mkdir(taskDir, { recursive: true })
|
||||
return taskDir
|
||||
return getGlobalStorageDir("tasks", taskId)
|
||||
}
|
||||
|
||||
export async function ensureRulesDirectoryExists(): Promise<string> {
|
||||
@@ -103,16 +100,11 @@ export async function ensureMcpServersDirectoryExists(): Promise<string> {
|
||||
return mcpServersDir
|
||||
}
|
||||
|
||||
export async function ensureSettingsDirectoryExists(context: vscode.ExtensionContext): Promise<string> {
|
||||
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
export async function ensureSettingsDirectoryExists(): Promise<string> {
|
||||
return getGlobalStorageDir("settings")
|
||||
}
|
||||
|
||||
export async function getSavedApiConversationHistory(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
): Promise<Anthropic.MessageParam[]> {
|
||||
export async function getSavedApiConversationHistory(taskId: string): Promise<Anthropic.MessageParam[]> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.apiConversationHistory)
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
if (fileExists) {
|
||||
@@ -180,13 +172,17 @@ export async function saveTaskMetadata(taskId: string, metadata: TaskMetadata) {
|
||||
}
|
||||
|
||||
export async function ensureStateDirectoryExists(): Promise<string> {
|
||||
const stateDir = path.join(HostProvider.get().globalStorageFsPath, "state")
|
||||
await fs.mkdir(stateDir, { recursive: true })
|
||||
return stateDir
|
||||
return getGlobalStorageDir("state")
|
||||
}
|
||||
|
||||
export async function ensureCacheDirectoryExists(): Promise<string> {
|
||||
return HostProvider.getGlobalStorageDir("cache")
|
||||
return getGlobalStorageDir("cache")
|
||||
}
|
||||
|
||||
async function getGlobalStorageDir(...subdirs: string[]) {
|
||||
const fullPath = path.resolve(HostProvider.get().globalStorageFsPath, ...subdirs)
|
||||
await fs.mkdir(fullPath, { recursive: true })
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export async function getTaskHistoryStateFilePath(): Promise<string> {
|
||||
@@ -246,7 +242,7 @@ export async function readTaskSettingsFromStorage(taskId: string): Promise<Parti
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeTaskSettingsToStorage(taskId: string, settings: Partial<GlobalState>) {
|
||||
export async function writeTaskSettingsToStorage(taskId: string, settings: Partial<Settings>) {
|
||||
try {
|
||||
const taskDirectoryFilePath = await ensureTaskDirectoryExists(taskId)
|
||||
const settingsFilePath = path.join(taskDirectoryFilePath, "settings.json")
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface GlobalState {
|
||||
primaryRootIndex: number
|
||||
multiRootEnabled: boolean
|
||||
lastDismissedInfoBannerVersion: number
|
||||
lastDismissedModelBannerVersion: number
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
|
||||
@@ -238,6 +238,9 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["mcpMarketplaceCatalog"]>("mcpMarketplaceCatalog")
|
||||
const lastDismissedInfoBannerVersion =
|
||||
context.globalState.get<GlobalStateAndSettings["lastDismissedInfoBannerVersion"]>("lastDismissedInfoBannerVersion")
|
||||
const lastDismissedModelBannerVersion = context.globalState.get<
|
||||
GlobalStateAndSettings["lastDismissedModelBannerVersion"]
|
||||
>("lastDismissedModelBannerVersion")
|
||||
const qwenCodeOauthPath = context.globalState.get<GlobalStateAndSettings["qwenCodeOauthPath"]>("qwenCodeOauthPath")
|
||||
const customPrompt = context.globalState.get<GlobalStateAndSettings["customPrompt"]>("customPrompt")
|
||||
const autoCondenseThreshold =
|
||||
@@ -557,6 +560,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
customPrompt,
|
||||
autoCondenseThreshold: autoCondenseThreshold || 0.75, // default to 0.75 if not set
|
||||
lastDismissedInfoBannerVersion: lastDismissedInfoBannerVersion ?? 0,
|
||||
lastDismissedModelBannerVersion: lastDismissedModelBannerVersion ?? 0,
|
||||
// Multi-root workspace support
|
||||
workspaceRoots,
|
||||
primaryRootIndex: primaryRootIndex ?? 0,
|
||||
|
||||
@@ -218,15 +218,10 @@ export class ToolExecutor {
|
||||
* Updates the browser settings
|
||||
*/
|
||||
public async applyLatestBrowserSettings() {
|
||||
if (this.context) {
|
||||
await this.browserSession.dispose()
|
||||
const apiHandlerModel = this.api.getModel()
|
||||
const useWebp = this.api ? !modelDoesntSupportWebp(apiHandlerModel) : true
|
||||
this.browserSession = new BrowserSession(this.context, this.stateManager, useWebp)
|
||||
} else {
|
||||
console.warn("no controller context available for browserSession")
|
||||
}
|
||||
|
||||
await this.browserSession.dispose()
|
||||
const apiHandlerModel = this.api.getModel()
|
||||
const useWebp = this.api ? !modelDoesntSupportWebp(apiHandlerModel) : true
|
||||
this.browserSession = new BrowserSession(this.stateManager, useWebp)
|
||||
return this.browserSession
|
||||
}
|
||||
|
||||
|
||||
+90
-73
@@ -55,9 +55,9 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@sha
|
||||
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { getGitRemoteUrls, getLatestGitCommitHash } from "@utils/git"
|
||||
import { isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
|
||||
import { arePathsEqual, getDesktopDir } from "@utils/path"
|
||||
import { filterExistingFiles } from "@utils/tabFiltering"
|
||||
import cloneDeep from "clone-deep"
|
||||
import { execa } from "execa"
|
||||
import pWaitFor from "p-wait-for"
|
||||
@@ -85,6 +85,27 @@ import { detectAvailableCliTools, updateApiReqMsg } from "./utils"
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
|
||||
type TaskParams = {
|
||||
controller: Controller
|
||||
mcpHub: McpHub
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
postStateToWebview: () => Promise<void>
|
||||
reinitExistingTaskFromId: (taskId: string) => Promise<void>
|
||||
cancelTask: () => Promise<void>
|
||||
shellIntegrationTimeout: number
|
||||
terminalReuseEnabled: boolean
|
||||
terminalOutputLineLimit: number
|
||||
defaultTerminalProfile: string
|
||||
cwd: string
|
||||
stateManager: StateManager
|
||||
workspaceManager?: WorkspaceRootManager
|
||||
task?: string
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
historyItem?: HistoryItem
|
||||
taskId: string
|
||||
}
|
||||
|
||||
export class Task {
|
||||
// Core task variables
|
||||
readonly taskId: string
|
||||
@@ -132,25 +153,28 @@ export class Task {
|
||||
// Workspace manager
|
||||
workspaceManager?: WorkspaceRootManager
|
||||
|
||||
constructor(
|
||||
controller: Controller,
|
||||
mcpHub: McpHub,
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
|
||||
postStateToWebview: () => Promise<void>,
|
||||
reinitExistingTaskFromId: (taskId: string) => Promise<void>,
|
||||
cancelTask: () => Promise<void>,
|
||||
shellIntegrationTimeout: number,
|
||||
terminalReuseEnabled: boolean,
|
||||
terminalOutputLineLimit: number,
|
||||
defaultTerminalProfile: string,
|
||||
cwd: string,
|
||||
stateManager: StateManager,
|
||||
workspaceManager?: WorkspaceRootManager,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
historyItem?: HistoryItem,
|
||||
) {
|
||||
constructor(params: TaskParams) {
|
||||
const {
|
||||
controller,
|
||||
mcpHub,
|
||||
updateTaskHistory,
|
||||
postStateToWebview,
|
||||
reinitExistingTaskFromId,
|
||||
cancelTask,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
cwd,
|
||||
stateManager,
|
||||
workspaceManager,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
historyItem,
|
||||
taskId,
|
||||
} = params
|
||||
|
||||
this.taskInitializationStartTime = performance.now()
|
||||
this.taskState = new TaskState()
|
||||
this.controller = controller
|
||||
@@ -179,7 +203,7 @@ export class Task {
|
||||
this.terminalManager.setDefaultTerminalProfile(defaultTerminalProfile)
|
||||
|
||||
this.urlContentFetcher = new UrlContentFetcher(controller.context)
|
||||
this.browserSession = new BrowserSession(controller.context, stateManager)
|
||||
this.browserSession = new BrowserSession(stateManager)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = HostProvider.get().createDiffViewProvider()
|
||||
this.cwd = cwd
|
||||
@@ -192,9 +216,10 @@ export class Task {
|
||||
await this.say("mcp_notification", `[${serverName}] ${message}`)
|
||||
})
|
||||
|
||||
this.taskId = taskId
|
||||
|
||||
// Initialize taskId first
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
this.ulid = historyItem.ulid ?? ulid()
|
||||
this.taskIsFavorited = historyItem.isFavorited
|
||||
this.taskState.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
|
||||
@@ -202,14 +227,12 @@ export class Task {
|
||||
this.taskState.checkpointManagerErrorMessage = historyItem.checkpointManagerErrorMessage
|
||||
}
|
||||
} else if (task || images || files) {
|
||||
this.taskId = Date.now().toString()
|
||||
this.ulid = ulid()
|
||||
} else {
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
|
||||
this.messageStateHandler = new MessageStateHandler({
|
||||
context: controller.context,
|
||||
taskId: this.taskId,
|
||||
ulid: this.ulid,
|
||||
taskState: this.taskState,
|
||||
@@ -243,7 +266,6 @@ export class Task {
|
||||
fileContextTracker: this.fileContextTracker,
|
||||
diffViewProvider: this.diffViewProvider,
|
||||
taskState: this.taskState,
|
||||
context: controller.context,
|
||||
workspaceManager: this.workspaceManager,
|
||||
updateTaskHistory: this.updateTaskHistory,
|
||||
say: this.say.bind(this),
|
||||
@@ -391,16 +413,6 @@ export class Task {
|
||||
this.taskState.consecutiveAutoApprovedRequestsCount = 0
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private getContext(): vscode.ExtensionContext {
|
||||
const context = this.controller.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Communicate with webview
|
||||
|
||||
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
|
||||
@@ -738,13 +750,11 @@ export class Task {
|
||||
|
||||
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldn't be initialized when opening a old task, and it was because we were waiting for resume)
|
||||
// This is important in case the user deletes messages without resuming the task first
|
||||
const context = this.getContext()
|
||||
const savedApiConversationHistory = await getSavedApiConversationHistory(context, this.taskId)
|
||||
const savedApiConversationHistory = await getSavedApiConversationHistory(this.taskId)
|
||||
this.messageStateHandler.setApiConversationHistory(savedApiConversationHistory)
|
||||
|
||||
// load the context history state
|
||||
|
||||
const _taskDir = await ensureTaskDirectoryExists(this.taskId)
|
||||
await ensureTaskDirectoryExists(this.taskId)
|
||||
await this.contextManager.initializeContextHistory(await ensureTaskDirectoryExists(this.taskId))
|
||||
|
||||
const lastClineMessage = this.messageStateHandler
|
||||
@@ -777,7 +787,6 @@ export class Task {
|
||||
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
|
||||
|
||||
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await getSavedApiConversationHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
)
|
||||
|
||||
@@ -1038,16 +1047,15 @@ export class Task {
|
||||
let outputBuffer: string[] = []
|
||||
let outputBufferSize: number = 0
|
||||
let chunkTimer: NodeJS.Timeout | null = null
|
||||
let chunkEnroute = false
|
||||
|
||||
// Track if buffer gets stuck
|
||||
// Track if buffer gets stuck (correlated with PROCESS_WHILE_RUNNING to indicate genuine technical issues)
|
||||
let bufferStuckTimer: NodeJS.Timeout | null = null
|
||||
const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
const flushBuffer = async (force = false) => {
|
||||
if (chunkEnroute || outputBuffer.length === 0) {
|
||||
if (force && !chunkEnroute && outputBuffer.length > 0) {
|
||||
// If force is true and no chunkEnroute, flush anyway
|
||||
if (outputBuffer.length === 0) {
|
||||
if (force) {
|
||||
// If force is true, flush anyway
|
||||
} else {
|
||||
return
|
||||
}
|
||||
@@ -1055,7 +1063,6 @@ export class Task {
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
chunkEnroute = true
|
||||
|
||||
// Start timer to detect if buffer gets stuck
|
||||
bufferStuckTimer = setTimeout(() => {
|
||||
@@ -1077,19 +1084,21 @@ export class Task {
|
||||
}
|
||||
didContinue = true
|
||||
process.continue()
|
||||
|
||||
// If more output accumulated, flush again
|
||||
if (outputBuffer.length > 0) {
|
||||
await flushBuffer()
|
||||
}
|
||||
} catch {
|
||||
Logger.error("Error while asking for command output")
|
||||
} finally {
|
||||
// If the command finishes execution before the 'command_output' ask promise resolves (in other words before the user responded to the ask, which is expected when the command finishes execution first), this block is reached. This is expected and safe to ignore, as no further handling is required.
|
||||
|
||||
// Clear the stuck timer
|
||||
if (bufferStuckTimer) {
|
||||
clearTimeout(bufferStuckTimer)
|
||||
bufferStuckTimer = null
|
||||
}
|
||||
chunkEnroute = false
|
||||
// If more output accumulated while chunkEnroute, flush again
|
||||
if (outputBuffer.length > 0) {
|
||||
await flushBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1753,19 +1762,30 @@ export class Task {
|
||||
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
|
||||
// then say "checkpoint_created" and perform the commit.
|
||||
if (isFirstRequest && this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting") && this.checkpointManager) {
|
||||
const commitHash = await this.checkpointManager.commit() // Actual commit
|
||||
await this.say("checkpoint_created") // Now this is conditional
|
||||
const lastCheckpointMessageIndex = findLastIndex(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
(m) => m.say === "checkpoint_created",
|
||||
)
|
||||
if (lastCheckpointMessageIndex !== -1) {
|
||||
await this.messageStateHandler.updateClineMessage(lastCheckpointMessageIndex, {
|
||||
lastCheckpointHash: commitHash,
|
||||
})
|
||||
// saveClineMessagesAndUpdateHistory will be called later after API response,
|
||||
// so no need to call it here unless this is the only modification to this message.
|
||||
// For now, assuming it's handled later.
|
||||
this.checkpointManager
|
||||
?.commit()
|
||||
.then(async (commitHash) => {
|
||||
if (commitHash) {
|
||||
await this.messageStateHandler.updateClineMessage(lastCheckpointMessageIndex, {
|
||||
lastCheckpointHash: commitHash,
|
||||
})
|
||||
// saveClineMessagesAndUpdateHistory will be called later after API response,
|
||||
// so no need to call it here unless this is the only modification to this message.
|
||||
// For now, assuming it's handled later.
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`[TaskCheckpointManager] Failed to create checkpoint commit for task ${this.taskId}:`,
|
||||
error,
|
||||
)
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
isFirstRequest &&
|
||||
@@ -2419,9 +2439,9 @@ export class Task {
|
||||
|
||||
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
|
||||
details += `\n\n# ${host.platform} Visible Files`
|
||||
const visibleFilePaths = (await HostProvider.window.getVisibleTabs({})).paths.map((absolutePath) =>
|
||||
path.relative(this.cwd, absolutePath),
|
||||
)
|
||||
const rawVisiblePaths = (await HostProvider.window.getVisibleTabs({})).paths
|
||||
const filteredVisiblePaths = await filterExistingFiles(rawVisiblePaths)
|
||||
const visibleFilePaths = filteredVisiblePaths.map((absolutePath) => path.relative(this.cwd, absolutePath))
|
||||
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedVisibleFiles = this.clineIgnoreController
|
||||
@@ -2436,9 +2456,9 @@ export class Task {
|
||||
}
|
||||
|
||||
details += `\n\n# ${host.platform} Open Tabs`
|
||||
const openTabPaths = (await HostProvider.window.getOpenTabs({})).paths.map((absolutePath) =>
|
||||
path.relative(this.cwd, absolutePath),
|
||||
)
|
||||
const rawOpenTabPaths = (await HostProvider.window.getOpenTabs({})).paths
|
||||
const filteredOpenTabPaths = await filterExistingFiles(rawOpenTabPaths)
|
||||
const openTabPaths = filteredOpenTabPaths.map((absolutePath) => path.relative(this.cwd, absolutePath))
|
||||
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedOpenTabs = this.clineIgnoreController
|
||||
@@ -2551,15 +2571,12 @@ export class Task {
|
||||
details += result
|
||||
}
|
||||
|
||||
// Add git remote URLs section
|
||||
const gitRemotes = await getGitRemoteUrls(this.cwd)
|
||||
if (gitRemotes.length > 0) {
|
||||
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
|
||||
}
|
||||
|
||||
const latestGitHash = await getLatestGitCommitHash(this.cwd)
|
||||
if (latestGitHash) {
|
||||
details += `\n\n# Latest Git Commit Hash\n${latestGitHash}`
|
||||
// Add workspace information in JSON format
|
||||
if (this.workspaceManager) {
|
||||
const workspacesJson = await this.workspaceManager.buildWorkspacesJson()
|
||||
if (workspacesJson) {
|
||||
details += `\n\n# Workspace Configuration\n${workspacesJson}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add detected CLI tools
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
|
||||
import getFolderSize from "get-folder-size"
|
||||
import * as vscode from "vscode"
|
||||
import { findLastIndex } from "@/shared/array"
|
||||
import { combineApiRequests } from "@/shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@/shared/combineCommandSequences"
|
||||
@@ -13,7 +12,6 @@ import { ensureTaskDirectoryExists, saveApiConversationHistory, saveClineMessage
|
||||
import { TaskState } from "./TaskState"
|
||||
|
||||
interface MessageStateHandlerParams {
|
||||
context: vscode.ExtensionContext
|
||||
taskId: string
|
||||
ulid: string
|
||||
taskIsFavorited?: boolean
|
||||
@@ -27,21 +25,17 @@ export class MessageStateHandler {
|
||||
private clineMessages: ClineMessage[] = []
|
||||
private taskIsFavorited: boolean
|
||||
private checkpointTracker: CheckpointTracker | undefined
|
||||
private checkpointManagerErrorMessage: string | undefined
|
||||
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
private context: vscode.ExtensionContext
|
||||
private taskId: string
|
||||
private ulid: string
|
||||
private taskState: TaskState
|
||||
|
||||
constructor(params: MessageStateHandlerParams) {
|
||||
this.context = params.context
|
||||
this.taskId = params.taskId
|
||||
this.ulid = params.ulid
|
||||
this.taskState = params.taskState
|
||||
this.taskIsFavorited = params.taskIsFavorited ?? false
|
||||
this.updateTaskHistory = params.updateTaskHistory
|
||||
this.checkpointManagerErrorMessage = this.taskState.checkpointManagerErrorMessage
|
||||
}
|
||||
|
||||
setCheckpointTracker(tracker: CheckpointTracker | undefined) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WorkspacePathAdapter } from "@core/workspace/WorkspacePathAdapter"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { fixModelHtmlEscaping } from "@utils/string"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
@@ -81,13 +82,17 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
let executionDir: string = config.cwd
|
||||
let actualCommand: string = command
|
||||
|
||||
let workspaceHintUsed = false
|
||||
let workspaceHint: string | undefined
|
||||
|
||||
if (config.isMultiRootEnabled && config.workspaceManager) {
|
||||
// Check if command has a workspace hint prefix
|
||||
// e.g., "@backend:npm install" or just "npm install"
|
||||
const commandMatch = command.match(/^@(\w+):(.+)$/)
|
||||
|
||||
if (commandMatch) {
|
||||
const workspaceHint = commandMatch[1]
|
||||
workspaceHintUsed = true
|
||||
workspaceHint = commandMatch[1]
|
||||
actualCommand = commandMatch[2].trim()
|
||||
|
||||
// Find the workspace root for this hint
|
||||
@@ -122,13 +127,35 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
? autoApproveResult
|
||||
: [autoApproveResult, false]
|
||||
|
||||
// Determine workspace context for telemetry
|
||||
const resolvedToNonPrimary = !arePathsEqual(executionDir, config.cwd)
|
||||
const workspaceContext = {
|
||||
isMultiRootEnabled: config.isMultiRootEnabled || false,
|
||||
usedWorkspaceHint: workspaceHintUsed,
|
||||
resolvedToNonPrimary,
|
||||
resolutionMethod: (workspaceHintUsed ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Capture workspace path resolution telemetry
|
||||
if (config.isMultiRootEnabled && config.workspaceManager) {
|
||||
telemetryService.captureWorkspacePathResolved(
|
||||
config.ulid,
|
||||
"ExecuteCommandToolHandler",
|
||||
workspaceHintUsed ? "hint_provided" : "fallback_to_primary",
|
||||
workspaceHintUsed ? "workspace_name" : undefined,
|
||||
resolvedToNonPrimary, // resolution success = resolved to different workspace
|
||||
undefined, // TODO: could calculate workspace index if needed
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if ((!requiresApprovalPerLLM && autoApproveSafe) || (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)) {
|
||||
// Auto-approve flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
|
||||
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
|
||||
config.taskState.consecutiveAutoApprovedRequestsCount++
|
||||
didAutoApprove = true
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
showNotificationForApprovalIfAutoApprovalEnabled(
|
||||
@@ -143,10 +170,17 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
config,
|
||||
)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
false,
|
||||
workspaceContext,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true, workspaceContext)
|
||||
}
|
||||
|
||||
// Setup timeout notification for long-running auto-approved commands
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "node:path"
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { listFiles } from "@services/glob/list-files"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
@@ -69,6 +70,15 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
const { absolutePath, displayPath } =
|
||||
typeof pathResult === "string" ? { absolutePath: pathResult, displayPath: relDirPath! } : pathResult
|
||||
|
||||
// Determine workspace context for telemetry
|
||||
const fallbackAbsolutePath = path.resolve(config.cwd, relDirPath ?? "")
|
||||
const workspaceContext = {
|
||||
isMultiRootEnabled: config.isMultiRootEnabled || false,
|
||||
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
|
||||
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
|
||||
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Execute the actual list files operation
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
|
||||
@@ -91,7 +101,7 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
config.taskState.consecutiveAutoApprovedRequestsCount++
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to view directory ${getWorkspaceBasename(absolutePath, "ListFilesToolHandler.notification")}/`
|
||||
@@ -107,10 +117,24 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
false,
|
||||
workspaceContext,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "node:path"
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { extractFileContent } from "@integrations/misc/extract-file-content"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineSayTool } from "@/shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
@@ -72,6 +73,15 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
const { absolutePath, displayPath } =
|
||||
typeof pathResult === "string" ? { absolutePath: pathResult, displayPath: relPath! } : pathResult
|
||||
|
||||
// Determine workspace context for telemetry
|
||||
const fallbackAbsolutePath = path.resolve(config.cwd, relPath ?? "")
|
||||
const workspaceContext = {
|
||||
isMultiRootEnabled: config.isMultiRootEnabled || false,
|
||||
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
|
||||
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
|
||||
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Handle approval flow
|
||||
const sharedMessageProps = {
|
||||
tool: "readFile",
|
||||
@@ -89,7 +99,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
config.taskState.consecutiveAutoApprovedRequestsCount++
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to read ${getWorkspaceBasename(absolutePath, "ReadFileToolHandler.notification")}`
|
||||
@@ -105,10 +115,24 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
false,
|
||||
workspaceContext,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { regexSearchFiles } from "@services/ripgrep"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { formatResponse } from "@/core/prompts/responses"
|
||||
import { parseWorkspaceInlinePath } from "@/core/workspace/utils/parseWorkspaceInlinePath"
|
||||
@@ -227,17 +227,68 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
// Determine which paths to search
|
||||
const searchPaths = this.determineSearchPaths(config, parsedPath, workspaceHint, relDirPath!)
|
||||
|
||||
// Determine workspace context for telemetry
|
||||
const primaryWorkspaceRoot = searchPaths[0]?.workspaceRoot
|
||||
const resolvedToNonPrimary =
|
||||
searchPaths.length === 0
|
||||
? true
|
||||
: searchPaths.length > 1 || (primaryWorkspaceRoot ? !arePathsEqual(primaryWorkspaceRoot, config.cwd) : true)
|
||||
const workspaceContext = {
|
||||
isMultiRootEnabled: config.isMultiRootEnabled || false,
|
||||
usedWorkspaceHint: !!workspaceHint,
|
||||
resolvedToNonPrimary,
|
||||
resolutionMethod: (workspaceHint ? "hint" : searchPaths.length > 1 ? "path_detection" : "primary_fallback") as
|
||||
| "hint"
|
||||
| "primary_fallback"
|
||||
| "path_detection",
|
||||
}
|
||||
|
||||
// Capture workspace path resolution telemetry
|
||||
if (config.isMultiRootEnabled && config.workspaceManager) {
|
||||
const resolutionType = workspaceHint
|
||||
? "hint_provided"
|
||||
: searchPaths.length > 1
|
||||
? "cross_workspace_search"
|
||||
: "fallback_to_primary"
|
||||
telemetryService.captureWorkspacePathResolved(
|
||||
config.ulid,
|
||||
"SearchFilesToolHandler",
|
||||
resolutionType,
|
||||
workspaceHint ? "workspace_name" : undefined,
|
||||
searchPaths.length > 0, // resolution success = found paths to search
|
||||
undefined, // TODO: could calculate primary workspace index
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// Execute searches in all relevant workspaces in parallel
|
||||
const searchPromises = searchPaths.map(({ absolutePath, workspaceName, workspaceRoot }) =>
|
||||
this.executeSearch(config, absolutePath, workspaceName, workspaceRoot, regex, filePattern),
|
||||
)
|
||||
|
||||
// Wait for all searches to complete
|
||||
const searchStartTime = performance.now()
|
||||
const searchResults = await Promise.all(searchPromises)
|
||||
const searchDurationMs = performance.now() - searchStartTime
|
||||
|
||||
// Format and combine results
|
||||
const results = this.formatSearchResults(config, searchResults, searchPaths)
|
||||
|
||||
// Capture workspace search pattern telemetry
|
||||
if (config.isMultiRootEnabled && config.workspaceManager) {
|
||||
const searchType = workspaceHint ? "targeted" : searchPaths.length > 1 ? "cross_workspace" : "primary_only"
|
||||
const resultsFound = searchResults.some((result) => result.resultCount > 0)
|
||||
|
||||
telemetryService.captureWorkspaceSearchPattern(
|
||||
config.ulid,
|
||||
searchType,
|
||||
searchPaths.length,
|
||||
!!workspaceHint,
|
||||
resultsFound,
|
||||
searchDurationMs,
|
||||
)
|
||||
}
|
||||
|
||||
const sharedMessageProps = {
|
||||
tool: "searchFiles",
|
||||
path: getReadablePath(config.cwd, relDirPath!),
|
||||
@@ -256,7 +307,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
config.taskState.consecutiveAutoApprovedRequestsCount++
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to search files for ${regex}`
|
||||
@@ -272,10 +323,24 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
false,
|
||||
workspaceContext,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from "node:path"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { constructNewFileContent } from "@core/assistant-message/diff"
|
||||
@@ -6,7 +7,7 @@ import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
@@ -48,7 +49,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
|
||||
try {
|
||||
const { relPath, fileExists, diff, content, newContent } = result
|
||||
const { relPath, absolutePath, fileExists, diff, content, newContent } = result
|
||||
|
||||
// Create and show partial UI message
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
@@ -71,7 +72,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
// CRITICAL: Open editor and stream content in real-time (from original code)
|
||||
if (!config.services.diffViewProvider.isEditing) {
|
||||
// Open the editor and prepare to stream content in
|
||||
await config.services.diffViewProvider.open(relPath)
|
||||
await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
|
||||
}
|
||||
// Editor is open, stream content in real-time (false = don't finalize yet)
|
||||
await config.services.diffViewProvider.update(newContent, false)
|
||||
@@ -121,7 +122,8 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
return "" // can only happen if the sharedLogic adds an error to userMessages
|
||||
}
|
||||
|
||||
const { relPath, fileExists, diff, content, newContent } = result
|
||||
const { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext } = result
|
||||
|
||||
|
||||
// Handle approval flow
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
@@ -137,7 +139,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
// show gui message before showing edit animation
|
||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||
await config.callbacks.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
|
||||
await config.services.diffViewProvider.open(relPath)
|
||||
await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
|
||||
}
|
||||
await config.services.diffViewProvider.update(newContent, true)
|
||||
await setTimeoutPromise(300) // wait for diff view to update
|
||||
@@ -163,7 +165,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
config.taskState.consecutiveAutoApprovedRequestsCount++
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
|
||||
|
||||
// we need an artificial delay to let the diagnostics catch up to the changes
|
||||
await setTimeoutPromise(3_500)
|
||||
@@ -212,7 +214,14 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
// await config.services.diffViewProvider.reset()
|
||||
|
||||
config.taskState.didRejectTool = true
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
false,
|
||||
workspaceContext,
|
||||
)
|
||||
|
||||
await config.services.diffViewProvider.revertChanges()
|
||||
return `The user denied this operation. ${fileDeniedNote}`
|
||||
@@ -234,7 +243,14 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
await config.callbacks.say("user_feedback", text, images, files)
|
||||
}
|
||||
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +320,15 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
? { absolutePath: pathResult, resolvedPath: relPath }
|
||||
: { absolutePath: pathResult.absolutePath, resolvedPath: pathResult.resolvedPath }
|
||||
|
||||
// Determine workspace context for telemetry
|
||||
const fallbackAbsolutePath = path.resolve(config.cwd, relPath)
|
||||
const workspaceContext = {
|
||||
isMultiRootEnabled: config.isMultiRootEnabled || false,
|
||||
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
|
||||
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
|
||||
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Check clineignore access first
|
||||
const accessValidation = this.validator.checkClineIgnorePath(resolvedPath)
|
||||
if (!accessValidation.ok) {
|
||||
@@ -350,7 +375,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
// open the editor if not done already. This is to fix diff error when model provides correct search-replace text but Cline throws error
|
||||
// because file is not open.
|
||||
if (!config.services.diffViewProvider.isEditing) {
|
||||
await config.services.diffViewProvider.open(relPath)
|
||||
await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -420,6 +445,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
|
||||
|
||||
return { relPath, fileExists, diff, content, newContent }
|
||||
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +1,42 @@
|
||||
import path from "node:path"
|
||||
import { Controller } from "@core/controller/index"
|
||||
import { findLast } from "@shared/array"
|
||||
import axios from "axios"
|
||||
import { readFile } from "fs/promises"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { getNonce } from "./getNonce"
|
||||
|
||||
export abstract class WebviewProvider {
|
||||
private static activeInstances: Set<WebviewProvider> = new Set()
|
||||
private static clientIdMap = new Map<WebviewProvider, string>()
|
||||
private static instance: WebviewProvider | null = null
|
||||
controller: Controller
|
||||
private clientId: string
|
||||
|
||||
private static lastActiveControllerId: string | null = null
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly providerType: WebviewProviderType,
|
||||
) {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.clientId = uuidv4()
|
||||
WebviewProvider.clientIdMap.set(this, this.clientId)
|
||||
constructor(readonly context: vscode.ExtensionContext) {
|
||||
WebviewProvider.instance = this
|
||||
|
||||
// Create controller with cache service
|
||||
this.controller = new Controller(context, this.clientId)
|
||||
WebviewProvider.setLastActiveControllerId(this.controller.id)
|
||||
}
|
||||
|
||||
// Add a method to get the client ID
|
||||
public getClientId(): string {
|
||||
return this.clientId
|
||||
}
|
||||
|
||||
// Add a static method to get the client ID for a specific instance
|
||||
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
|
||||
return WebviewProvider.clientIdMap.get(instance)
|
||||
this.controller = new Controller(context)
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
await this.controller.dispose()
|
||||
WebviewProvider.activeInstances.delete(this)
|
||||
// Remove from client ID map
|
||||
WebviewProvider.clientIdMap.delete(this)
|
||||
WebviewProvider.instance = null
|
||||
}
|
||||
|
||||
public static getInstance(): WebviewProvider {
|
||||
if (!WebviewProvider.instance) {
|
||||
throw new Error("WebviewProvider instance not initialized. Make sure to create a WebviewProvider instance first.")
|
||||
}
|
||||
return WebviewProvider.instance
|
||||
}
|
||||
|
||||
public static getVisibleInstance(): WebviewProvider | undefined {
|
||||
return findLast(Array.from(WebviewProvider.activeInstances), (instance) => instance.isVisible() === true)
|
||||
}
|
||||
|
||||
public static getActiveInstance(): WebviewProvider | undefined {
|
||||
return Array.from(WebviewProvider.activeInstances).find((instance) => instance.isActive())
|
||||
}
|
||||
|
||||
protected abstract isActive(): boolean
|
||||
|
||||
public static getAllInstances(): WebviewProvider[] {
|
||||
return Array.from(WebviewProvider.activeInstances)
|
||||
}
|
||||
|
||||
public static getSidebarInstance() {
|
||||
return Array.from(WebviewProvider.activeInstances).find(
|
||||
(instance) => instance.providerType === WebviewProviderType.SIDEBAR,
|
||||
)
|
||||
}
|
||||
|
||||
public static getTabInstances(): WebviewProvider[] {
|
||||
return Array.from(WebviewProvider.activeInstances).filter((instance) => instance.providerType === WebviewProviderType.TAB)
|
||||
}
|
||||
|
||||
public static getLastActiveInstance(): WebviewProvider | undefined {
|
||||
const lastActiveId = WebviewProvider.getLastActiveControllerId()
|
||||
if (!lastActiveId) {
|
||||
return undefined
|
||||
}
|
||||
return Array.from(WebviewProvider.activeInstances).find((instance) => instance.controller.id === lastActiveId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last active controller ID with performance optimization
|
||||
* @returns The last active controller ID or null
|
||||
*/
|
||||
public static getLastActiveControllerId(): string | null {
|
||||
return WebviewProvider.lastActiveControllerId || WebviewProvider.getSidebarInstance()?.controller.id || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last active controller ID with validation and performance optimization
|
||||
* @param controllerId The controller ID to set as last active
|
||||
*/
|
||||
public static setLastActiveControllerId(controllerId: string | null): void {
|
||||
// Only update if the value is actually different to avoid unnecessary operations
|
||||
if (WebviewProvider.lastActiveControllerId !== controllerId) {
|
||||
WebviewProvider.lastActiveControllerId = controllerId
|
||||
}
|
||||
return WebviewProvider.instance?.isVisible() ? WebviewProvider.instance : undefined
|
||||
}
|
||||
|
||||
public static async disposeAllInstances() {
|
||||
const instances = Array.from(WebviewProvider.activeInstances)
|
||||
for (const instance of instances) {
|
||||
await instance.dispose()
|
||||
if (WebviewProvider.instance) {
|
||||
await WebviewProvider.instance.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,13 +120,6 @@ export abstract class WebviewProvider {
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript" nonce="${nonce}">
|
||||
// Inject the provider type
|
||||
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
|
||||
|
||||
// Inject the client ID
|
||||
window.clineClientId = "${this.clientId}";
|
||||
</script>
|
||||
<script type="module" nonce="${nonce}" src="${scriptUrl}"></script>
|
||||
<script src="http://localhost:8097"></script>
|
||||
</body>
|
||||
@@ -292,13 +219,6 @@ export abstract class WebviewProvider {
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript" nonce="${nonce}">
|
||||
// Inject the provider type
|
||||
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
|
||||
|
||||
// Inject the client ID
|
||||
window.clineClientId = "${this.clientId}";
|
||||
</script>
|
||||
${reactRefresh}
|
||||
<script type="module" src="${scriptUrl}"></script>
|
||||
</body>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { execa } from "execa"
|
||||
import * as path from "path"
|
||||
import { getLatestGitCommitHash } from "../../utils/git"
|
||||
import { getGitRemoteUrls, getLatestGitCommitHash } from "../../utils/git"
|
||||
import { VcsType, WorkspaceRoot } from "./WorkspaceRoot"
|
||||
|
||||
export interface WorkspaceContext {
|
||||
@@ -219,6 +219,33 @@ export class WorkspaceRootManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build workspaces JSON structure for environment details
|
||||
*/
|
||||
async buildWorkspacesJson(): Promise<string | null> {
|
||||
const workspaces: Record<string, { hint: string; associatedRemoteUrls?: string[]; latestGitCommitHash?: string }> = {}
|
||||
|
||||
// Process all workspace roots
|
||||
for (const root of this.roots) {
|
||||
const hint = root.name || path.basename(root.path)
|
||||
const gitRemotes = await getGitRemoteUrls(root.path)
|
||||
const gitCommitHash = await getLatestGitCommitHash(root.path)
|
||||
|
||||
workspaces[root.path] = {
|
||||
hint,
|
||||
...(gitRemotes.length > 0 && { associatedRemoteUrls: gitRemotes }),
|
||||
...(gitCommitHash && { latestGitCommitHash: gitCommitHash }),
|
||||
}
|
||||
}
|
||||
|
||||
// Only return JSON if there's content to feed the env details
|
||||
if (Object.keys(workspaces).length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JSON.stringify({ workspaces }, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in Task and Controller
|
||||
|
||||
@@ -50,6 +50,14 @@ describe("parseWorkspaceInlinePath", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("should treat bare workspace hint as root path", () => {
|
||||
const result = parseWorkspaceInlinePath("@backend:")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "backend",
|
||||
relPath: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should trim whitespace", () => {
|
||||
const result = parseWorkspaceInlinePath("@ frontend : src/index.ts ")
|
||||
expect(result).to.deep.equal({
|
||||
|
||||
@@ -40,7 +40,7 @@ export function parseWorkspaceInlinePath(value: string): ParsedWorkspacePath {
|
||||
// Captures:
|
||||
// - Group 1: workspace name (anything except colon)
|
||||
// - Group 2: the path after the colon
|
||||
const match = value.match(/^@([^:]+):(.+)$/)
|
||||
const match = value.match(/^@([^:]+):(.*)$/)
|
||||
|
||||
if (match) {
|
||||
const [, workspaceHint, relPath] = match
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { EmptyRequest } from "@/shared/proto/cline/common"
|
||||
|
||||
/**
|
||||
* Checks if the current workspace has multiple root folders open.
|
||||
* This is a lightweight check that only counts workspace folders,
|
||||
* independent of feature flags or internal multi-root implementation status.
|
||||
*
|
||||
* Use this when you need to know the actual workspace state (e.g., for telemetry,
|
||||
* headers, or UI display), not whether multi-root features are enabled.
|
||||
*
|
||||
* @returns true if 2 or more workspace folders are open, false otherwise
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isMultiRoot = await isMultiRootWorkspace()
|
||||
* console.log(`User has ${isMultiRoot ? 'multiple' : 'single'} workspace folders open`)
|
||||
* ```
|
||||
*/
|
||||
export async function isMultiRootWorkspace(): Promise<boolean> {
|
||||
try {
|
||||
const workspacePaths = await HostProvider.workspace.getWorkspacePaths(EmptyRequest.create({}))
|
||||
return workspacePaths.paths.length > 1
|
||||
} catch (error) {
|
||||
console.error("Failed to detect multi-root workspace", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
* Registers development-only commands for task manipulation.
|
||||
* These are only activated in development mode.
|
||||
*/
|
||||
export function registerTaskCommands(context: vscode.ExtensionContext, controller: Controller): vscode.Disposable[] {
|
||||
export function registerTaskCommands(controller: Controller): vscode.Disposable[] {
|
||||
return [
|
||||
vscode.commands.registerCommand("cline.dev.createTestTasks", async () => {
|
||||
const count = (
|
||||
|
||||
@@ -10,7 +10,7 @@ export function createClineAPI(sidebarController: Controller): ClineAPI {
|
||||
await sidebarController.clearTask()
|
||||
await sidebarController.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(sidebarController.id)
|
||||
await sendChatButtonClickedEvent()
|
||||
await sidebarController.initTask(task, images)
|
||||
HostProvider.get().logToChannel(
|
||||
`Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)`,
|
||||
|
||||
+41
-175
@@ -2,9 +2,7 @@
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
|
||||
import assert from "node:assert"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import { DIFF_VIEW_URI_SCHEME } from "@hosts/vscode/VscodeDiffViewProvider"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/cline/ui"
|
||||
import * as vscode from "vscode"
|
||||
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
|
||||
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
|
||||
@@ -15,7 +13,6 @@ import { WebviewProvider } from "./core/webview"
|
||||
import { createClineAPI } from "./exports"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import { cleanupTestMode, initializeTestMode } from "./services/test/TestMode"
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import path from "node:path"
|
||||
@@ -32,9 +29,9 @@ import { sendAddToInputEvent } from "./core/controller/ui/subscribeToAddToInput"
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { workspaceResolver } from "./core/workspace"
|
||||
import { focusChatInput, getContextForCommand } from "./hosts/vscode/commandUtils"
|
||||
import { abortCommitGeneration, generateCommitMessage } from "./hosts/vscode/commit-message-generator"
|
||||
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
|
||||
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { GitCommitGenerator } from "./integrations/git/commit-message-generator"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { telemetryService } from "./services/telemetry"
|
||||
@@ -55,18 +52,18 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/framewo
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
setupHostProvider(context)
|
||||
|
||||
const sidebarWebview = (await initialize(context)) as VscodeWebviewProvider
|
||||
const webview = (await initialize(context)) as VscodeWebviewProvider
|
||||
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
const testModeWatchers = await initializeTestMode(sidebarWebview)
|
||||
const testModeWatchers = await initializeTestMode(webview)
|
||||
// Initialize test mode and add disposables to context
|
||||
context.subscriptions.push(...testModeWatchers)
|
||||
|
||||
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(VscodeWebviewProvider.SIDEBAR_ID, sidebarWebview, {
|
||||
vscode.window.registerWebviewViewProvider(VscodeWebviewProvider.SIDEBAR_ID, webview, {
|
||||
webviewOptions: { retainContextWhenHidden: true },
|
||||
}),
|
||||
)
|
||||
@@ -74,146 +71,52 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const { commands } = ExtensionRegistryInfo
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.PlusButton, async (webview: any) => {
|
||||
console.log("[DEBUG] plusButtonClicked", webview)
|
||||
// Pass the webview type to the event sender
|
||||
const isSidebar = !webview
|
||||
vscode.commands.registerCommand(commands.PlusButton, async () => {
|
||||
console.log("[DEBUG] plusButtonClicked")
|
||||
|
||||
const openChat = async (instance: WebviewProvider) => {
|
||||
await instance?.controller.clearTask()
|
||||
await instance?.controller.postStateToWebview()
|
||||
await sendChatButtonClickedEvent(instance.controller.id)
|
||||
}
|
||||
|
||||
if (isSidebar) {
|
||||
const sidebarInstance = WebviewProvider.getSidebarInstance()
|
||||
if (sidebarInstance) {
|
||||
openChat(sidebarInstance)
|
||||
// Send event to the sidebar instance
|
||||
}
|
||||
} else {
|
||||
const tabInstances = WebviewProvider.getTabInstances()
|
||||
for (const instance of tabInstances) {
|
||||
openChat(instance)
|
||||
}
|
||||
}
|
||||
const sidebarInstance = WebviewProvider.getInstance()
|
||||
await sidebarInstance.controller.clearTask()
|
||||
await sidebarInstance.controller.postStateToWebview()
|
||||
await sendChatButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.McpButton, (webview: any) => {
|
||||
console.log("[DEBUG] mcpButtonClicked", webview)
|
||||
|
||||
const activeInstance = WebviewProvider.getActiveInstance()
|
||||
const isSidebar = !webview
|
||||
|
||||
if (isSidebar) {
|
||||
const sidebarInstance = WebviewProvider.getSidebarInstance()
|
||||
const sidebarInstanceId = sidebarInstance?.getClientId()
|
||||
if (sidebarInstanceId) {
|
||||
sendMcpButtonClickedEvent(sidebarInstanceId)
|
||||
} else {
|
||||
console.error("[DEBUG] No sidebar instance found, cannot send MCP button event")
|
||||
}
|
||||
} else {
|
||||
const activeInstanceId = activeInstance?.getClientId()
|
||||
if (activeInstanceId) {
|
||||
sendMcpButtonClickedEvent(activeInstanceId)
|
||||
} else {
|
||||
console.error("[DEBUG] No active instance found, cannot send MCP button event")
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const openClineInNewTab = async () => {
|
||||
Logger.log("Opening Cline in new tab")
|
||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||
const tabWebview = HostProvider.get().createWebviewProvider(WebviewProviderType.TAB) as VscodeWebviewProvider
|
||||
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
|
||||
|
||||
// Check if there are any visible text editors, otherwise open a new group to the right
|
||||
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
|
||||
if (!hasVisibleEditors) {
|
||||
await vscode.commands.executeCommand("workbench.action.newGroupRight")
|
||||
}
|
||||
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(VscodeWebviewProvider.TAB_PANEL_ID, "Cline", targetCol, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.file(HostProvider.get().extensionFsPath)],
|
||||
})
|
||||
// TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath)
|
||||
|
||||
panel.iconPath = {
|
||||
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_light.png"),
|
||||
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_dark.png"),
|
||||
}
|
||||
tabWebview.resolveWebviewView(panel)
|
||||
|
||||
// Lock the editor group so clicking on files doesn't open them over the panel
|
||||
await setTimeoutPromise(100)
|
||||
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
|
||||
return tabWebview
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand(commands.PopoutButton, openClineInNewTab))
|
||||
context.subscriptions.push(vscode.commands.registerCommand(commands.OpenInNewTab, openClineInNewTab))
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.SettingsButton, (webview: any) => {
|
||||
const isSidebar = !webview
|
||||
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
|
||||
|
||||
sendSettingsButtonClickedEvent(webviewType)
|
||||
vscode.commands.registerCommand(commands.McpButton, () => {
|
||||
sendMcpButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.HistoryButton, async (webview: any) => {
|
||||
console.log("[DEBUG] historyButtonClicked", webview)
|
||||
// Pass the webview type to the event sender
|
||||
const isSidebar = !webview
|
||||
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
|
||||
vscode.commands.registerCommand(commands.SettingsButton, () => {
|
||||
sendSettingsButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.HistoryButton, async () => {
|
||||
// Send event to all subscribers using the gRPC streaming method
|
||||
await sendHistoryButtonClickedEvent(webviewType)
|
||||
await sendHistoryButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.AccountButton, (webview: any) => {
|
||||
console.log("[DEBUG] accountButtonClicked", webview)
|
||||
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
const sidebarInstance = WebviewProvider.getSidebarInstance()
|
||||
if (sidebarInstance) {
|
||||
// Send event to sidebar controller
|
||||
sendAccountButtonClickedEvent(sidebarInstance.controller.id)
|
||||
}
|
||||
} else {
|
||||
// Send to all tab instances
|
||||
const tabInstances = WebviewProvider.getTabInstances()
|
||||
for (const instance of tabInstances) {
|
||||
sendAccountButtonClickedEvent(instance.controller.id)
|
||||
}
|
||||
}
|
||||
vscode.commands.registerCommand(commands.AccountButton, () => {
|
||||
// Send event to all subscribers using the gRPC streaming method
|
||||
sendAccountButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
/*
|
||||
We use the text document content provider API to show the left side for diff view by creating a
|
||||
virtual document for the original content. This makes it readonly so users know to edit the right
|
||||
We use the text document content provider API to show the left side for diff view by creating a
|
||||
virtual document for the original content. This makes it readonly so users know to edit the right
|
||||
side if they want to keep their changes.
|
||||
|
||||
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by
|
||||
claiming an uri-scheme for which your provider then returns text contents. The scheme must be
|
||||
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by
|
||||
claiming an uri-scheme for which your provider then returns text contents. The scheme must be
|
||||
provided when registering a provider and cannot change afterwards.
|
||||
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents
|
||||
given such an uri. In return, content providers are wired into the open document logic so that
|
||||
given such an uri. In return, content providers are wired into the open document logic so that
|
||||
providers are always considered.
|
||||
https://code.visualstudio.com/api/extension-guides/virtual-documents
|
||||
*/
|
||||
@@ -238,7 +141,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Use dynamic import to avoid loading the module in production
|
||||
import("./dev/commands/tasks")
|
||||
.then((module) => {
|
||||
const devTaskCommands = module.registerTaskCommands(context, sidebarWebview.controller)
|
||||
const devTaskCommands = module.registerTaskCommands(webview.controller)
|
||||
context.subscriptions.push(...devTaskCommands)
|
||||
Logger.log("Cline dev task commands registered")
|
||||
})
|
||||
@@ -428,54 +331,17 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Register the focusChatInput command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.FocusChatInput, async () => {
|
||||
// Fast path: check for existing active instance
|
||||
let activeWebview = WebviewProvider.getLastActiveInstance() as VscodeWebviewProvider
|
||||
const webview = WebviewProvider.getInstance() as VscodeWebviewProvider
|
||||
|
||||
if (activeWebview) {
|
||||
// Instance exists - just reveal and focus it
|
||||
const webview = activeWebview.getWebview()
|
||||
if (webview) {
|
||||
if (webview && "reveal" in webview) {
|
||||
webview.reveal()
|
||||
} else if ("show" in webview) {
|
||||
webview.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No active instance - need to find or create one
|
||||
WebviewProvider.setLastActiveControllerId(null)
|
||||
|
||||
// Check for existing tab instances first (cheaper than focusing sidebar)
|
||||
const tabInstances = WebviewProvider.getTabInstances() as VscodeWebviewProvider[]
|
||||
if (tabInstances.length > 0) {
|
||||
activeWebview = tabInstances[tabInstances.length - 1]
|
||||
} else {
|
||||
// Try to focus sidebar via hostbridge
|
||||
await HostProvider.workspace.openClineSidebarPanel({})
|
||||
|
||||
// Small delay for focus to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
activeWebview = WebviewProvider.getSidebarInstance() as VscodeWebviewProvider
|
||||
if (!activeWebview) {
|
||||
// Last resort: create new tab
|
||||
activeWebview = (await openClineInNewTab()) as VscodeWebviewProvider
|
||||
}
|
||||
}
|
||||
// Show the webview
|
||||
const webviewView = webview.getWebview()
|
||||
if (webviewView) {
|
||||
webviewView.show()
|
||||
}
|
||||
|
||||
// Send focus event
|
||||
const clientId = activeWebview?.getClientId()
|
||||
if (!clientId) {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sendFocusChatInputEvent(clientId)
|
||||
telemetryService.captureButtonClick("command_focusChatInput", activeWebview.controller?.task?.ulid)
|
||||
sendFocusChatInputEvent()
|
||||
telemetryService.captureButtonClick("command_focusChatInput", webview.controller?.task?.ulid)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -499,10 +365,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Register the generateGitCommitMessage command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.GenerateCommit, async (scm) => {
|
||||
await GitCommitGenerator?.generate?.(context, scm)
|
||||
generateCommitMessage(webview.controller.stateManager, scm)
|
||||
}),
|
||||
vscode.commands.registerCommand(commands.AbortCommit, () => {
|
||||
GitCommitGenerator?.abort?.()
|
||||
abortCommitGeneration()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -511,8 +377,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
if (event.key === "clineAccountId") {
|
||||
// Check if the secret was removed (logout) or added/updated (login)
|
||||
const secretValue = await context.secrets.get("clineAccountId")
|
||||
const activeWebviewProvider = WebviewProvider.getVisibleInstance()
|
||||
const controller = activeWebviewProvider?.controller
|
||||
const activeWebview = WebviewProvider.getVisibleInstance()
|
||||
const controller = activeWebview?.controller
|
||||
|
||||
const authService = AuthService.getInstance(controller)
|
||||
if (secretValue) {
|
||||
@@ -526,13 +392,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
return createClineAPI(sidebarWebview.controller)
|
||||
return createClineAPI(webview.controller)
|
||||
}
|
||||
|
||||
function setupHostProvider(context: ExtensionContext) {
|
||||
console.log("Setting up vscode host providers...")
|
||||
|
||||
const createWebview = (type: WebviewProviderType) => new VscodeWebviewProvider(context, type)
|
||||
const createWebview = () => new VscodeWebviewProvider(context)
|
||||
const createDiffView = () => new VscodeDiffViewProvider()
|
||||
const outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import * as vscode from "vscode"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
|
||||
export class ExternalWebviewProvider extends WebviewProvider {
|
||||
// This hostname cannot be changed without updating the external webview handler.
|
||||
private RESOURCE_HOSTNAME: string = "internal.resources"
|
||||
|
||||
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
|
||||
super(context, providerType)
|
||||
}
|
||||
|
||||
override getWebviewUrl(path: string) {
|
||||
const url = new URL(`https://${this.RESOURCE_HOSTNAME}/`)
|
||||
url.pathname = path
|
||||
@@ -21,7 +15,4 @@ export class ExternalWebviewProvider extends WebviewProvider {
|
||||
override isVisible() {
|
||||
return true
|
||||
}
|
||||
protected override isActive(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { HostBridgeClientProvider } from "./host-provider-types"
|
||||
/**
|
||||
* Singleton class that manages host-specific providers for dependency injection.
|
||||
@@ -127,27 +124,12 @@ export class HostProvider {
|
||||
public static get diff() {
|
||||
return HostProvider.get().hostBridge.diffClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global storage directory for the extension, or a sub-directory of the global storage dir.
|
||||
* If the directory does not exist, it is created.
|
||||
* @param subdirs
|
||||
* @returns
|
||||
*/
|
||||
public static async getGlobalStorageDir(subdirs?: string) {
|
||||
if (!subdirs) {
|
||||
return HostProvider.get().globalStorageFsPath
|
||||
}
|
||||
const fullPath = path.resolve(HostProvider.get().globalStorageFsPath, subdirs)
|
||||
await fs.mkdir(fullPath, { recursive: true })
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that creates WebviewProvider instances
|
||||
*/
|
||||
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
|
||||
export type WebviewProviderCreator = () => WebviewProvider
|
||||
|
||||
/**
|
||||
* A function that creates DiffViewProvider instances
|
||||
|
||||
@@ -6,7 +6,6 @@ import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { WebviewMessage } from "@/shared/WebviewMessage"
|
||||
import type { WebviewProviderType } from "@/shared/webview/types"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -17,15 +16,10 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
// Used in package.json as the view's id. This value cannot be changed due to how vscode caches
|
||||
// views based on their id, and updating the id would break existing instances of the extension.
|
||||
public static readonly SIDEBAR_ID = ExtensionRegistryInfo.views.Sidebar
|
||||
public static readonly TAB_PANEL_ID = ExtensionRegistryInfo.views.TabPanel
|
||||
|
||||
private webview?: vscode.WebviewView | vscode.WebviewPanel
|
||||
private webview?: vscode.WebviewView
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
|
||||
super(context, providerType)
|
||||
}
|
||||
|
||||
override getWebviewUrl(path: string) {
|
||||
if (!this.webview) {
|
||||
throw new Error("Webview not initialized")
|
||||
@@ -41,28 +35,21 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
return this.webview.webview.cspSource
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
if (this.webview && this.webview.viewType === VscodeWebviewProvider.TAB_PANEL_ID && "active" in this.webview) {
|
||||
return this.webview.active === true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override isVisible() {
|
||||
return this.webview?.visible || false
|
||||
}
|
||||
|
||||
public getWebview(): vscode.WebviewView | vscode.WebviewPanel | undefined {
|
||||
public getWebview(): vscode.WebviewView | undefined {
|
||||
return this.webview
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and sets up the webview when it's first created.
|
||||
*
|
||||
* @param webviewView - The webview view or panel instance to be resolved
|
||||
* @param webviewView - The sidebar webview view instance to be resolved
|
||||
* @returns A promise that resolves when the webview has been fully initialized
|
||||
*/
|
||||
public async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise<void> {
|
||||
public async resolveWebviewView(webviewView: vscode.WebviewView): Promise<void> {
|
||||
this.webview = webviewView
|
||||
|
||||
webviewView.webview.options = {
|
||||
@@ -83,43 +70,26 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
// Logs show up in bottom panel > Debug Console
|
||||
//console.log("registering listener")
|
||||
|
||||
// Listen for when the panel becomes visible
|
||||
// Listen for when the sidebar becomes visible
|
||||
// https://github.com/microsoft/vscode-discussions/discussions/840
|
||||
if ("onDidChangeViewState" in webviewView) {
|
||||
// WebviewView and WebviewPanel have all the same properties except for this visibility listener
|
||||
// panel
|
||||
webviewView.onDidChangeViewState(
|
||||
async (e) => {
|
||||
if (e?.webviewPanel?.visible && e.webviewPanel?.active) {
|
||||
WebviewProvider.setLastActiveControllerId(this.controller.id)
|
||||
// Only send the event if the webview is active (focused)
|
||||
await sendDidBecomeVisibleEvent(this.controller.id)
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
)
|
||||
} else if ("onDidChangeVisibility" in webviewView) {
|
||||
// sidebar
|
||||
webviewView.onDidChangeVisibility(
|
||||
async () => {
|
||||
if (this.webview?.visible) {
|
||||
WebviewProvider.setLastActiveControllerId(this.controller.id)
|
||||
await sendDidBecomeVisibleEvent(this.controller.id)
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
)
|
||||
}
|
||||
|
||||
// onDidChangeVisibility is only available on the sidebar webview
|
||||
// Otherwise WebviewView and WebviewPanel have all the same properties except for this visibility listener
|
||||
// WebviewPanel is not currently used in the extension
|
||||
webviewView.onDidChangeVisibility(
|
||||
async () => {
|
||||
if (this.webview?.visible) {
|
||||
await sendDidBecomeVisibleEvent()
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
)
|
||||
|
||||
// Listen for when the view is disposed
|
||||
// This happens when the user closes the view or when the view is closed programmatically
|
||||
webviewView.onDidDispose(
|
||||
async () => {
|
||||
if (WebviewProvider.getLastActiveControllerId() === this.controller.id) {
|
||||
WebviewProvider.setLastActiveControllerId(null)
|
||||
}
|
||||
await this.dispose()
|
||||
},
|
||||
null,
|
||||
@@ -219,9 +189,8 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
}
|
||||
|
||||
override async dispose() {
|
||||
if (this.webview && "dispose" in this.webview) {
|
||||
this.webview.dispose()
|
||||
}
|
||||
// WebviewView doesn't have a dispose method, it's managed by VSCode
|
||||
// We just need to clean up our disposables
|
||||
while (this.disposables.length) {
|
||||
const x = this.disposables.pop()
|
||||
if (x) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as vscode from "vscode"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { CommandContext } from "@/shared/proto/index.cline"
|
||||
@@ -23,10 +22,7 @@ export async function getContextForCommand(
|
||||
}
|
||||
> {
|
||||
const activeWebview = await focusChatInput()
|
||||
if (!activeWebview) {
|
||||
return
|
||||
}
|
||||
// Use the controller from the last active instance
|
||||
// Use the controller from the active instance
|
||||
const controller = activeWebview.controller
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
@@ -50,16 +46,9 @@ export async function getContextForCommand(
|
||||
return { controller, commandContext }
|
||||
}
|
||||
|
||||
export async function focusChatInput(): Promise<WebviewProvider | undefined> {
|
||||
export async function focusChatInput(): Promise<WebviewProvider> {
|
||||
await vscode.commands.executeCommand(ExtensionRegistryInfo.commands.FocusChatInput)
|
||||
|
||||
// Wait for a webview instance to become available after focusing
|
||||
await pWaitFor(() => !!WebviewProvider.getLastActiveInstance())
|
||||
const activeWebview = WebviewProvider.getLastActiveInstance()
|
||||
if (!activeWebview) {
|
||||
console.error("No active webview to receive command")
|
||||
return
|
||||
}
|
||||
|
||||
return activeWebview
|
||||
// At this point, the instance is guaranteed to exist due to the FocusChatInput command
|
||||
return WebviewProvider.getInstance()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user