mirror of
https://github.com/cline/cline.git
synced 2026-09-12 17:19:32 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e21a466ae |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Enhance Test Workflow and Report Coverage to Qlty on Main
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: update vercel provider cost note and sign-up url
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix repeated API error 400 in SAP AI Core provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add us-west-1 to Amazon Bedrock regions
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/.github/ @saoudrizwan
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
|
||||
+147
-78
@@ -1,9 +1,6 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -17,45 +14,7 @@ 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:
|
||||
@@ -74,6 +33,18 @@ 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
|
||||
@@ -81,6 +52,7 @@ 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
|
||||
@@ -96,60 +68,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"
|
||||
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- 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
|
||||
- name: Build Tests and Extension
|
||||
run: npm run ci:build
|
||||
run: npm run pretest
|
||||
|
||||
- name: Unit Tests with coverage - Linux
|
||||
id: unit_tests_linux
|
||||
continue-on-error: true
|
||||
if: runner.os == 'Linux'
|
||||
- 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
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
continue-on-error: true
|
||||
if: runner.os != 'Linux'
|
||||
run: |
|
||||
else
|
||||
npm run test:unit
|
||||
fi
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
id: extension_coverage
|
||||
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: |
|
||||
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
|
||||
|
||||
# Run webview tests with coverage
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_tests
|
||||
id: webview_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd webview-ui
|
||||
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"
|
||||
# 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
|
||||
|
||||
# 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
|
||||
@@ -157,11 +129,27 @@ 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
|
||||
@@ -224,6 +212,85 @@ 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
|
||||
@@ -231,6 +298,8 @@ 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
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -74,6 +74,7 @@ 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
|
||||
|
||||
|
||||
+26
-32
@@ -25,15 +25,15 @@
|
||||
},
|
||||
"styling": {
|
||||
"eyebrows": "breadcrumbs",
|
||||
"codeblocks": "system",
|
||||
"css": "styles.css"
|
||||
"codeblocks": "system"
|
||||
},
|
||||
"appearance": {
|
||||
"default": "system",
|
||||
"strict": false
|
||||
},
|
||||
"fonts": {
|
||||
"family": "Roboto"
|
||||
"family": "Roboto",
|
||||
"weight": 400
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
@@ -58,8 +58,9 @@
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"getting-started/what-is-cline",
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/installing-cline-jetbrains",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
{
|
||||
@@ -81,6 +82,16 @@
|
||||
{
|
||||
"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": [
|
||||
@@ -92,10 +103,16 @@
|
||||
"features/at-mentions/url-mentions"
|
||||
]
|
||||
},
|
||||
"features/auto-approve",
|
||||
"features/auto-compact",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -112,24 +129,7 @@
|
||||
"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,12 +232,6 @@
|
||||
"url": "getting-started/what-is-cline"
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/getting-started/installing-cline-jetbrains",
|
||||
"destination": "/getting-started/installing-cline"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
"prompt": "Search Cline documentation..."
|
||||
},
|
||||
|
||||
+32
-121
@@ -1,149 +1,60 @@
|
||||
---
|
||||
title: "Dictation"
|
||||
description: "Communicate with Cline using your voice for faster, more natural AI collaboration"
|
||||
title: Dictation
|
||||
description:
|
||||
---
|
||||
|
||||
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.
|
||||
Cline lets you transcribe speech to text in an easy, built-in service
|
||||
|
||||
## Why Voice Changes Everything
|
||||
## Get Started
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
## Settings
|
||||
|
||||
The friction of typing was holding back real collaboration. Voice removes that friction.
|
||||
Enable or disable dictation in Feature Settings by toggling "Enable Dictation." You can also change the transcription language to one of the supported languages.
|
||||
|
||||
## Getting Started
|
||||
## Requirements
|
||||
|
||||
**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)
|
||||
Cline uses FFmpeg to capture your voice across all platforms:
|
||||
|
||||
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`)
|
||||
- **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.
|
||||
|
||||
## Where Dictation Shines
|
||||
## Technical Details
|
||||
|
||||
### Plan Mode Conversations
|
||||
### Independent from Chat Provider
|
||||
|
||||
Dictation is perfect for [Plan mode](/features/plan-and-act) discussions. Instead of carefully crafting prompts, you can:
|
||||
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.
|
||||
|
||||
- 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
|
||||
### Audio Format
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Complex Problem Explanation
|
||||
### Privacy & Security
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Microphone Not Working**
|
||||
- Check your IDE permissions for microphone access
|
||||
- Ensure FFmpeg is properly installed
|
||||
- Try refreshing VSCode/your editor
|
||||
`Failed to start recording` - Audio recording tools aren't installed. Cline will prompt you to install FFmpeg - just follow the chat instructions.
|
||||
|
||||
**Poor Transcription Quality**
|
||||
- Speak clearly and at normal volume
|
||||
- Reduce background noise if possible
|
||||
- Check your microphone settings
|
||||
`Invalid audio format or request data` - Usually an audio recording issue. Make sure FFmpeg is properly installed and working.
|
||||
|
||||
**Connection Issues**
|
||||
- Verify internet connection
|
||||
- Check if firewall is blocking Cline's servers
|
||||
- Try signing out and back into your Cline account
|
||||
`Authentication failed` - You need to reauthenticate your Cline account. Sign out and back in, then check your internet connection.
|
||||
|
||||
**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
|
||||
`Insufficient credits for transcription service` - Your Cline account doesn't have enough credits. Check your balance and purchase more if needed.
|
||||
|
||||
**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
|
||||
`Cannot connect to transcription service` - Connection issue. Check your internet connection and firewall settings aren't blocking Cline's servers.
|
||||
|
||||
## The Future of AI Collaboration
|
||||
## API Usage
|
||||
|
||||
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.
|
||||
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.
|
||||
@@ -24,10 +24,6 @@ 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:
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
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).
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
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,254 +1,81 @@
|
||||
---
|
||||
title: "Installing Cline"
|
||||
description: "Get Cline set up in your editor and start building projects with AI assistance."
|
||||
description: "Cline brings AI-powered coding assistance to your editor. Available for VS Code and JetBrains IDEs."
|
||||
---
|
||||
|
||||
## 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 IDEs. Select your preferred editor below for installation instructions:
|
||||
Cline works across multiple development environments:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="VS Code/Cursor" icon="code">
|
||||
### Installation Steps
|
||||
- **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
|
||||
|
||||
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 Installation
|
||||
|
||||
<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>
|
||||
### VS Code Marketplace: Step-by-Step Setup
|
||||
|
||||
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"
|
||||
Follow these steps to get Cline up and running:
|
||||
|
||||
> **Note:** If VS Code shows "Running extensions might..." dialog, click "Allow". If you don't see the Cline icon, restart VS Code.
|
||||
1. **Open VS Code:** Launch the VS Code application.
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
> **Note:** If VS Code shows "Running extensions might...", click "Allow".
|
||||
|
||||
**Plugin Installation Issues**
|
||||
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`.
|
||||
|
||||
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
|
||||
<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 installation fails:
|
||||
- Restart your VS Code and try again
|
||||
- Check your internet connection
|
||||
- Try installing from VSIX file as an alternative
|
||||
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.
|
||||
|
||||
**Plugin Not Appearing**
|
||||
> **Pro Tip:** You should see the Cline chat window appear in your VS Code editor!
|
||||
|
||||
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)
|
||||
### Open VSX Registry
|
||||
|
||||
**Common Issues**
|
||||
For VS Code-compatible editors without Marketplace access (like VSCodium and Windsurf):
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
## JetBrains Installation
|
||||
|
||||
Need help? Join our [Discord community](https://discord.gg/cline).
|
||||
For IntelliJ IDEA, PyCharm, WebStorm, DataSpell, and other JetBrains IDEs:
|
||||
|
||||
</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" }}
|
||||
/>
|
||||
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
|
||||
|
||||
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.
|
||||
**Need more help?** See our [complete JetBrains installation guide](/getting-started/installing-cline-jetbrains) for screenshots and troubleshooting.
|
||||
|
||||
### Creating Your Cline 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
|
||||
Now that you have Cline installed, let's get you set up with your account:
|
||||
|
||||
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
|
||||
|
||||
@@ -269,4 +96,4 @@ Hey Cline! Could you help me create a new project folder called "hello-world" in
|
||||
|
||||
### Still Struggling?
|
||||
|
||||
Join our [Discord community](https://discord.gg/cline) and engage with our team and other Cline users directly.
|
||||
Join our Discord community 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.5** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
|
||||
| **Claude Sonnet 4** | 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.5 |
|
||||
| Something that just works | Claude Sonnet 4 |
|
||||
| To save money | DeepSeek V3 or Qwen3 variants |
|
||||
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4.5 |
|
||||
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4 |
|
||||
| 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.5** 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** 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.5: 1,000,000 tokens
|
||||
- Claude Sonnet 4: 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.5)
|
||||
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4)
|
||||
|
||||
### When to Watch the Bar
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "What is Cline?"
|
||||
description: "An introduction to Cline, your AI-powered development assistant for modern IDEs."
|
||||
description: "An introduction to Cline, your AI-powered development assistant in VS Code."
|
||||
---
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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) 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.
|
||||
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.
|
||||
|
||||
@@ -18,8 +18,11 @@ Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `anthropic/claude-sonnet-4.5` (Recommended)
|
||||
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-sonnet-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `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`
|
||||
@@ -44,8 +47,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 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.
|
||||
- **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.
|
||||
- **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).
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
Generated
+442
-1739
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -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.4",
|
||||
"version": "3.32.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -356,10 +356,9 @@
|
||||
"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:coverage": "vscode-test --coverage",
|
||||
@@ -410,22 +409,24 @@
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"archiver": "^7.0.1",
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "5.6.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"glob": "^11.0.3",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"minimatch": "^3.1.2",
|
||||
"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",
|
||||
"rimraf": "^6.0.1",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"tar": "^7.5.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^2.6.1",
|
||||
@@ -458,8 +459,8 @@
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
|
||||
@@ -19,6 +19,7 @@ service StateService {
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
|
||||
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
|
||||
}
|
||||
message DictationSettings {
|
||||
bool feature_enabled = 1;
|
||||
@@ -302,3 +303,10 @@ message Viewport {
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
|
||||
message ProcessInfo {
|
||||
int32 process_id = 1;
|
||||
optional string version = 2;
|
||||
optional int64 uptime_ms = 3;
|
||||
}
|
||||
@@ -31,6 +31,9 @@ service EnvService {
|
||||
|
||||
// Returns events when the telemetry settings change.
|
||||
rpc subscribeToTelemetrySettings(cline.EmptyRequest) returns (stream TelemetrySettingsEvent);
|
||||
|
||||
// Initiates a graceful shutdown of the host bridge service.
|
||||
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/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)
|
||||
}
|
||||
@@ -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_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -44,8 +44,10 @@ export class AnthropicHandler implements ApiHandler {
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
|
||||
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 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 budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = !!(
|
||||
|
||||
@@ -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_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_4_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_1M_SUFFIX)
|
||||
? rawModelId.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
const modelId = rawModelId.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
? rawModelId.slice(0, -CLAUDE_SONNET_4_1M_SUFFIX.length)
|
||||
: rawModelId
|
||||
|
||||
const enable1mContextWindow = rawModelId.endsWith(CLAUDE_SONNET_1M_SUFFIX)
|
||||
const enable1mContextWindow = rawModelId.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
|
||||
|
||||
const model = this.getModel()
|
||||
|
||||
|
||||
+19
-40
@@ -1,13 +1,8 @@
|
||||
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
|
||||
@@ -78,11 +73,9 @@ describe("Retry Decorator", () => {
|
||||
|
||||
it("should respect retry-after header with delta seconds", async () => {
|
||||
let callCount = 0
|
||||
const setTimeoutSpy = sinon.spy(global, "setTimeout")
|
||||
const baseDelay = 1000
|
||||
|
||||
const startTime = Date.now()
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay }) // Use large baseDelay to ensure header takes precedence
|
||||
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -101,23 +94,19 @@ 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 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
|
||||
const startTime = Date.now()
|
||||
const retryTimestamp = Math.floor(Date.now() / 1000) + 0.01 // 10ms in the future
|
||||
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay }) // Use large baseDelay to ensure header takes precedence
|
||||
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -136,22 +125,17 @@ 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 baseDelay = 10
|
||||
|
||||
const startTime = Date.now()
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 2, baseDelay, maxDelay: 100 })
|
||||
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
@@ -169,22 +153,18 @@ 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 baseDelay = 50
|
||||
const maxDelay = 10
|
||||
|
||||
const startTime = Date.now()
|
||||
class TestClass {
|
||||
@withRetry({ maxRetries: 3, baseDelay, maxDelay })
|
||||
@withRetry({ maxRetries: 3, baseDelay: 50, maxDelay: 10 })
|
||||
async *failMethod() {
|
||||
callCount++
|
||||
if (callCount < 3) {
|
||||
@@ -202,11 +182,10 @@ 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.length > 0 ? reasoningDetails : undefined,
|
||||
reasoning_details: reasoningDetails,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import {
|
||||
CLAUDE_SONNET_1M_SUFFIX,
|
||||
ModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import { CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo, openRouterClaudeSonnet41mModelId } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
@@ -24,10 +19,10 @@ export async function createOpenRouterStream(
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
const isClaudeSonnet41m = model.id === openRouterClaudeSonnet41mModelId
|
||||
if (isClaudeSonnet41m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_4_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
@@ -35,7 +30,6 @@ export async function createOpenRouterStream(
|
||||
// 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":
|
||||
@@ -96,7 +90,6 @@ export async function createOpenRouterStream(
|
||||
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":
|
||||
@@ -134,7 +127,6 @@ 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":
|
||||
@@ -180,7 +172,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
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...(isClaudeSonnet41m ? { provider: { order: ["anthropic", "amazon-bedrock"], allow_fallbacks: false } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -5,12 +5,7 @@ import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import {
|
||||
CLAUDE_SONNET_1M_TIERS,
|
||||
clineCodeSupernovaModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@/shared/api"
|
||||
import { CLAUDE_SONNET_4_1M_TIERS, clineCodeSupernovaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
|
||||
import { Controller } from ".."
|
||||
|
||||
type OpenRouterSupportedParams =
|
||||
@@ -114,7 +109,6 @@ 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
|
||||
@@ -220,14 +214,11 @@ export async function refreshOpenRouterModels(
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
// add custom :1m model variant
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { ProcessInfo } from "@shared/proto/cline/state"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Gets process information including PID, version, and uptime
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns ProcessInfo with process details
|
||||
*/
|
||||
export async function getProcessInfo(controller: Controller, request: EmptyRequest): Promise<ProcessInfo> {
|
||||
// Get the current state to access the version (same source as webview)
|
||||
const state = await controller.getStateToPostToWebview()
|
||||
|
||||
return ProcessInfo.create({
|
||||
processId: process.pid,
|
||||
version: state.version || "unknown",
|
||||
uptimeMs: Math.floor(process.uptime() * 1000), // Convert seconds to milliseconds
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import Database from "better-sqlite3"
|
||||
import { existsSync, mkdirSync, unlinkSync } from "fs"
|
||||
import * as path from "path"
|
||||
import type { InstanceLockData, SqliteLockManagerOptions } from "./types"
|
||||
|
||||
export class SqliteLockManager {
|
||||
private db!: Database.Database
|
||||
private instanceAddress: string
|
||||
private dbPath: string
|
||||
private readonly STALE_LOCK_TIMEOUT = 1 * 60 * 1000 // 1 minute in milliseconds
|
||||
|
||||
constructor(options: SqliteLockManagerOptions) {
|
||||
this.instanceAddress = options.instanceAddress
|
||||
this.dbPath = options.dbPath
|
||||
|
||||
// Ensure the directory exists before creating the database
|
||||
const dbDir = path.dirname(this.dbPath)
|
||||
try {
|
||||
mkdirSync(dbDir, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error(`CRITICAL ERROR: Failed to create SQLite database directory ${dbDir}:`, error)
|
||||
throw new Error(`Failed to create SQLite database directory: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
this.initializeDatabaseWithLockSync()
|
||||
} catch (error) {
|
||||
console.error(`CRITICAL ERROR: Failed to initialize SQLite database at ${this.dbPath}:`, error)
|
||||
throw new Error(`Failed to initialize SQLite database: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDatabaseWithLockSync(): void {
|
||||
const lockFile = `${this.dbPath}.lock`
|
||||
|
||||
// Clean up stale lock files first
|
||||
this.cleanupStaleLockSync(lockFile)
|
||||
|
||||
try {
|
||||
// Try to acquire exclusive file lock for database creation
|
||||
const fs = require("fs")
|
||||
let fd: number | null = null
|
||||
|
||||
try {
|
||||
fd = fs.openSync(lockFile, "wx") // Exclusive creation - fails if file exists
|
||||
|
||||
// Write timestamp to lock file for stale lock detection
|
||||
fs.writeFileSync(fd, Date.now().toString())
|
||||
|
||||
// Check if database already exists
|
||||
const dbExists = existsSync(this.dbPath)
|
||||
|
||||
if (!dbExists) {
|
||||
// Database doesn't exist, create it
|
||||
this.db = new Database(this.dbPath)
|
||||
this.initializeDatabase()
|
||||
} else {
|
||||
// Database exists, just open it
|
||||
this.db = new Database(this.dbPath)
|
||||
}
|
||||
} finally {
|
||||
// Always clean up the lock file
|
||||
if (fd !== null) {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
try {
|
||||
unlinkSync(lockFile)
|
||||
} catch {} // Ignore errors if file was already deleted
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === "EEXIST") {
|
||||
// Another process is initializing the database, wait and retry
|
||||
const delay = 100 + Math.random() * 100 // Add jitter
|
||||
this.sleepSync(delay)
|
||||
this.initializeDatabaseWithLockSync()
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private sleepSync(ms: number) {
|
||||
// Non-spinning, synchronous sleep using Atomics.wait
|
||||
// Works in Node main thread (since v12.16+) and worker threads.
|
||||
const sab = new SharedArrayBuffer(4)
|
||||
const ia = new Int32Array(sab)
|
||||
Atomics.wait(ia, 0, 0, Math.max(0, Math.floor(ms)))
|
||||
}
|
||||
|
||||
private cleanupStaleLockSync(lockFile: string): void {
|
||||
try {
|
||||
if (!existsSync(lockFile)) {
|
||||
return // Lock file doesn't exist, nothing to clean up
|
||||
}
|
||||
|
||||
const fs = require("fs")
|
||||
|
||||
try {
|
||||
const timestampStr = fs.readFileSync(lockFile, "utf8").trim()
|
||||
const timestamp = parseInt(timestampStr, 10)
|
||||
|
||||
if (isNaN(timestamp) || Date.now() - timestamp > this.STALE_LOCK_TIMEOUT) {
|
||||
// Stale lock, remove it
|
||||
unlinkSync(lockFile)
|
||||
console.warn(`Removed stale database lock file: ${lockFile}`)
|
||||
}
|
||||
} catch (readError) {
|
||||
// If we can't read the timestamp, assume it's stale
|
||||
unlinkSync(lockFile)
|
||||
console.warn(`Removed unreadable database lock file: ${lockFile}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
// Lock file doesn't exist, which is fine
|
||||
console.warn(`Error checking lock file ${lockFile}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDatabase() {
|
||||
// Create the locks table with the unified schema (matches cli/pkg/common/schema.go)
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS locks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
held_by TEXT NOT NULL,
|
||||
lock_type TEXT NOT NULL CHECK (lock_type IN ('file', 'instance', 'folder')),
|
||||
lock_target TEXT NOT NULL,
|
||||
locked_at INTEGER NOT NULL,
|
||||
UNIQUE(lock_type, lock_target)
|
||||
);
|
||||
`)
|
||||
|
||||
// Create indexes for performance (matches cli/pkg/common/schema.go)
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_held_by ON locks(held_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_type ON locks(lock_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_locks_target ON locks(lock_target);
|
||||
`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this instance in the locks table
|
||||
*/
|
||||
async registerInstance(data: {
|
||||
corePort: number
|
||||
hostPort: number
|
||||
version?: string
|
||||
status?: InstanceLockData["status"]
|
||||
}): Promise<void> {
|
||||
const now = Date.now()
|
||||
const hostAddress = `localhost:${data.hostPort}`
|
||||
|
||||
// Create instance lock entry
|
||||
const insertLock = this.db.prepare(`
|
||||
INSERT OR REPLACE INTO locks (held_by, lock_type, lock_target, locked_at)
|
||||
VALUES (?, 'instance', ?, ?)
|
||||
`)
|
||||
|
||||
insertLock.run(this.instanceAddress, hostAddress, now)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timestamp for this instance (touch)
|
||||
*/
|
||||
touchInstance(): void {
|
||||
const now = Date.now()
|
||||
const updateLock = this.db.prepare(`
|
||||
UPDATE locks
|
||||
SET locked_at = ?
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
updateLock.run(now, this.instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this instance from the locks table
|
||||
*/
|
||||
unregisterInstance(): void {
|
||||
const deleteLock = this.db.prepare(`
|
||||
DELETE FROM locks
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
deleteLock.run(this.instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the registry for any instance registered on the given port
|
||||
*/
|
||||
getInstanceByPort(port: number): { instanceAddress: string; hostAddress: string } | null {
|
||||
const query = this.db.prepare(`
|
||||
SELECT held_by, lock_target
|
||||
FROM locks
|
||||
WHERE lock_type = 'instance'
|
||||
AND (held_by LIKE '%:' || ? OR lock_target LIKE '%:' || ?)
|
||||
`)
|
||||
|
||||
const result = query.get(port, port) as { held_by: string; lock_target: string } | undefined
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
instanceAddress: result.held_by,
|
||||
hostAddress: result.lock_target,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific instance entry from the registry
|
||||
*/
|
||||
removeInstanceByAddress(instanceAddress: string): void {
|
||||
const deleteLock = this.db.prepare(`
|
||||
DELETE FROM locks
|
||||
WHERE held_by = ? AND lock_type = 'instance'
|
||||
`)
|
||||
|
||||
deleteLock.run(instanceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type LockType = "file" | "instance" | "folder"
|
||||
|
||||
export type LockStatus = "starting" | "healthy" | "unhealthy"
|
||||
|
||||
export interface LockRow {
|
||||
id: number
|
||||
held_by: string // address:port of instance holding the lock
|
||||
lock_type: LockType
|
||||
lock_target: string // varies by type: file path, host address, or folder path
|
||||
locked_at: number // timestamp when lock was acquired
|
||||
}
|
||||
|
||||
export interface InstanceLockData {
|
||||
address: string
|
||||
core_port: number
|
||||
host_port: number
|
||||
status: LockStatus
|
||||
last_seen: string
|
||||
process_pid: number
|
||||
version?: string
|
||||
created_at: string
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface SqliteLockManagerOptions {
|
||||
dbPath: string
|
||||
instanceAddress: string // host:port format
|
||||
}
|
||||
+3
-28
@@ -31,7 +31,6 @@ import {
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
} from "@core/storage/disk"
|
||||
import { VcsType } from "@core/workspace"
|
||||
import { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
|
||||
import { buildCheckpointManager, shouldUseMultiRoot } from "@integrations/checkpoints/factory"
|
||||
import { ensureCheckpointInitialized } from "@integrations/checkpoints/initializer"
|
||||
@@ -2549,33 +2548,9 @@ export class Task {
|
||||
}
|
||||
|
||||
// Add git remote URLs section
|
||||
const isMultiRootEnabled = featureFlagsService.getMultiRootEnabled()
|
||||
const workspaceRoots = this.workspaceManager?.getRoots() ?? []
|
||||
|
||||
if (isMultiRootEnabled && workspaceRoots.length > 1) {
|
||||
const remoteSections: string[] = []
|
||||
for (const root of workspaceRoots) {
|
||||
if (root.vcs !== VcsType.Git) {
|
||||
continue
|
||||
}
|
||||
|
||||
const gitRemotes = await getGitRemoteUrls(root.path)
|
||||
if (gitRemotes.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const rootLabel = root.name || path.basename(root.path)
|
||||
remoteSections.push(`## ${rootLabel}\n${gitRemotes.join("\n")}`)
|
||||
}
|
||||
|
||||
if (remoteSections.length > 0) {
|
||||
details += `\n\n# Git Remote URLs\n${remoteSections.join("\n\n")}`
|
||||
}
|
||||
} else {
|
||||
const gitRemotes = await getGitRemoteUrls(this.cwd)
|
||||
if (gitRemotes.length > 0) {
|
||||
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -208,23 +208,11 @@ export class OcaAuthService {
|
||||
await this.sendAuthStatusUpdate()
|
||||
|
||||
// Avoid repeated/looping login attempts
|
||||
if (this._interactiveLoginPending) {
|
||||
return
|
||||
}
|
||||
if (this._interactiveLoginPending) return
|
||||
this._interactiveLoginPending = true
|
||||
try {
|
||||
// Kickstart interactive login (opens browser)
|
||||
await this.createAuthRequest()
|
||||
// Wait up to 60 seconds for user to complete login
|
||||
const timeoutMs = 60_000
|
||||
const pollMs = 250
|
||||
const start = Date.now()
|
||||
while (!this._authenticated && Date.now() - start < timeoutMs) {
|
||||
await new Promise((r) => setTimeout(r, pollMs))
|
||||
}
|
||||
if (!this._authenticated) {
|
||||
console.warn("Interactive OCA login timed out after 120 seconds")
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to initiate interactive OCA login:", e)
|
||||
} finally {
|
||||
|
||||
+18
-50
@@ -240,8 +240,8 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo {
|
||||
surveyContent?: string
|
||||
}
|
||||
|
||||
export const CLAUDE_SONNET_1M_SUFFIX = ":1m"
|
||||
export const CLAUDE_SONNET_1M_TIERS = [
|
||||
export const CLAUDE_SONNET_4_1M_SUFFIX = ":1m"
|
||||
export const CLAUDE_SONNET_4_1M_TIERS = [
|
||||
{
|
||||
contextWindow: 200000,
|
||||
inputPrice: 3.0,
|
||||
@@ -274,27 +274,6 @@ export const anthropicModels = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-sonnet-4-5-20250929:1m": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-sonnet-4-20250514:1m": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_000_000,
|
||||
@@ -304,7 +283,18 @@ export const anthropicModels = {
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
tiers: CLAUDE_SONNET_4_1M_TIERS,
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-opus-4-1-20250805": {
|
||||
maxTokens: 8192,
|
||||
@@ -431,7 +421,7 @@ export const bedrockModels = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0:1m": {
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0:1m": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
@@ -440,7 +430,7 @@ export const bedrockModels = {
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
tiers: CLAUDE_SONNET_4_1M_TIERS,
|
||||
},
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0": {
|
||||
maxTokens: 8192,
|
||||
@@ -452,17 +442,6 @@ export const bedrockModels = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0:1m": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"anthropic.claude-opus-4-20250514-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -622,8 +601,7 @@ export const bedrockModels = {
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "anthropic/claude-sonnet-4.5" // will always exist in openRouterModels
|
||||
export const openRouterClaudeSonnet41mModelId = `anthropic/claude-sonnet-4${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeSonnet451mModelId = `anthropic/claude-sonnet-4.5${CLAUDE_SONNET_1M_SUFFIX}`
|
||||
export const openRouterClaudeSonnet41mModelId = `anthropic/claude-sonnet-4${CLAUDE_SONNET_4_1M_SUFFIX}`
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -652,18 +630,8 @@ export const clineCodeSupernovaModelInfo: ModelInfo = {
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models
|
||||
export type VertexModelId = keyof typeof vertexModels
|
||||
export const vertexDefaultModelId: VertexModelId = "claude-sonnet-4@20250514" // TODO: update to 4-5
|
||||
export const vertexDefaultModelId: VertexModelId = "claude-sonnet-4@20250514"
|
||||
export const vertexModels = {
|
||||
"claude-sonnet-4-5@20250929": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-sonnet-4@20250514": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
|
||||
+240
-18
@@ -2,37 +2,200 @@ import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvid
|
||||
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
|
||||
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
||||
import { WebviewProviderType } from "@shared/webview/types"
|
||||
import * as path from "path"
|
||||
import { retryOperation } from "@utils/retry"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { initialize, tearDown } from "@/common"
|
||||
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
import { waitForHostBridgeReady } from "./hostbridge-client"
|
||||
import { startProtobusService } from "./protobus-service"
|
||||
import { log } from "./utils"
|
||||
import { DATA_DIR, EXTENSION_DIR, extensionContext } from "./vscode-context"
|
||||
import { checkPortAvailability } from "./port-checker"
|
||||
import { startProtobusService, waitForHostBridgeReady } from "./protobus-service"
|
||||
import { log, SETTINGS_SUBFOLDER } from "./utils"
|
||||
import { createExtensionContext } from "./vscode-context"
|
||||
|
||||
// Default ports
|
||||
export const DEFAULT_PROTOBUS_PORT = 26040
|
||||
export const DEFAULT_HOSTBRIDGE_PORT = 26041
|
||||
|
||||
// Parse command line arguments
|
||||
interface CliArgs {
|
||||
port?: number
|
||||
hostBridgePort?: number
|
||||
config?: string
|
||||
help?: boolean
|
||||
}
|
||||
|
||||
function parseArgs(): CliArgs {
|
||||
const args: CliArgs = {}
|
||||
const argv = process.argv.slice(2)
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
switch (arg) {
|
||||
case "--port":
|
||||
case "-p":
|
||||
args.port = parseInt(argv[++i], 10)
|
||||
break
|
||||
case "--host-bridge-port":
|
||||
args.hostBridgePort = parseInt(argv[++i], 10)
|
||||
break
|
||||
case "--config":
|
||||
case "-c":
|
||||
args.config = argv[++i]
|
||||
break
|
||||
case "--help":
|
||||
case "-h":
|
||||
args.help = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Cline Core - Standalone Server
|
||||
|
||||
Usage: node cline-core.js [options]
|
||||
|
||||
Options:
|
||||
-p, --port <port> Port for the main gRPC service (default: ${DEFAULT_PROTOBUS_PORT})
|
||||
--host-bridge-port <port> Port for the host bridge service (default: ${DEFAULT_HOSTBRIDGE_PORT})
|
||||
-c, --config <path> Directory for Cline data storage (default: ~/.cline or CLINE_DIR env var)
|
||||
-h, --help Show this help message
|
||||
|
||||
Environment Variables:
|
||||
PROTOBUS_ADDRESS Override the main service address (format: host:port)
|
||||
HOSTBRIDGE_ADDRESS Override the host bridge address (format: host:port)
|
||||
CLINE_DIR Default Cline data directory (overridden by --config flag)
|
||||
`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Parse command line arguments
|
||||
const args = parseArgs()
|
||||
|
||||
// Show help if requested
|
||||
if (args.help) {
|
||||
showHelp()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Configure ports from arguments or env vars
|
||||
let protobusPort = DEFAULT_PROTOBUS_PORT
|
||||
let hostBridgePort = DEFAULT_HOSTBRIDGE_PORT
|
||||
|
||||
if (args.port) {
|
||||
protobusPort = args.port
|
||||
// If only port is specified, calculate hostbridge port as port + 1000
|
||||
if (!args.hostBridgePort) {
|
||||
hostBridgePort = protobusPort + 1000
|
||||
}
|
||||
}
|
||||
if (args.hostBridgePort) {
|
||||
hostBridgePort = args.hostBridgePort
|
||||
}
|
||||
|
||||
// Set environment variables for the services to use
|
||||
if (!process.env.PROTOBUS_ADDRESS) {
|
||||
process.env.PROTOBUS_ADDRESS = `localhost:${protobusPort}`
|
||||
}
|
||||
if (!process.env.HOSTBRIDGE_ADDRESS) {
|
||||
process.env.HOSTBRIDGE_ADDRESS = `localhost:${hostBridgePort}`
|
||||
}
|
||||
|
||||
// Configure Cline directory from CLI args, env var, or default
|
||||
// Priority: --config flag > CLINE_DIR env var > ~/.cline default
|
||||
const clineDir = args.config || process.env.CLINE_DIR || `${os.homedir()}/.cline`
|
||||
|
||||
log("\n\n\nStarting cline-core service...\n\n\n")
|
||||
log(`Using Protobus port: ${protobusPort}`)
|
||||
log(`Using Host Bridge port: ${hostBridgePort}`)
|
||||
log(`Using Cline directory: ${clineDir}`)
|
||||
|
||||
await waitForHostBridgeReady()
|
||||
// Initialize SQLite lock manager for instance registration
|
||||
const dbPath = `${clineDir}/${SETTINGS_SUBFOLDER}/locks.db`
|
||||
// Use host:port everywhere (no scheme)
|
||||
const fullAddress = `localhost:${protobusPort}`
|
||||
let lockManager: SqliteLockManager | undefined
|
||||
try {
|
||||
lockManager = new SqliteLockManager({
|
||||
dbPath,
|
||||
instanceAddress: fullAddress,
|
||||
})
|
||||
|
||||
// The host bridge should be available before creating the host provider because it depends on the host bridge.
|
||||
setupHostProvider()
|
||||
// Check port availability before proceeding
|
||||
log(`Checking port availability for ${protobusPort}...`)
|
||||
const portCheck = await checkPortAvailability(protobusPort, lockManager)
|
||||
|
||||
if (!portCheck.canProceed) {
|
||||
log(`STARTUP BLOCKED: ${portCheck.error}`)
|
||||
lockManager.close()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await lockManager.registerInstance({
|
||||
corePort: protobusPort,
|
||||
hostPort: hostBridgePort,
|
||||
version: process.env.CLINE_VERSION,
|
||||
status: "starting",
|
||||
})
|
||||
log(`Registered instance in SQLite locks: ${fullAddress}`)
|
||||
} catch (err) {
|
||||
log(`CRITICAL ERROR: Failed to register instance in SQLite locks: ${String(err)}`)
|
||||
log(`This is a fatal error - cline-core cannot start without proper instance registration`)
|
||||
if (lockManager) {
|
||||
try {
|
||||
lockManager.close()
|
||||
} catch {}
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForHostBridgeReady()
|
||||
log("HostBridge is serving; continuing startup")
|
||||
} catch (err) {
|
||||
log(`ERROR: HostBridge error: ${String(err)}`)
|
||||
// Cleanup lock manager entry if startup fails
|
||||
if (lockManager) {
|
||||
try {
|
||||
lockManager.unregisterInstance()
|
||||
lockManager.close()
|
||||
} catch {}
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create extension context with the configured directory
|
||||
const extensionContext = createExtensionContext(clineDir)
|
||||
|
||||
// Get EXTENSION_DIR and DATA_DIR from the extension context for use by HostProvider
|
||||
const EXTENSION_DIR = extensionContext.extensionPath
|
||||
const DATA_DIR = extensionContext.globalStoragePath
|
||||
|
||||
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR)
|
||||
|
||||
// Set up global error handlers to prevent process crashes
|
||||
setupGlobalErrorHandlers()
|
||||
setupGlobalErrorHandlers(lockManager)
|
||||
|
||||
const webviewProvider = await initialize(extensionContext)
|
||||
|
||||
// Enable the localhost HTTP server that handles auth redirects.
|
||||
AuthHandler.getInstance().setEnabled(true)
|
||||
|
||||
startProtobusService(webviewProvider.controller)
|
||||
|
||||
// Mark instance healthy after services are up
|
||||
try {
|
||||
lockManager?.touchInstance()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function setupHostProvider() {
|
||||
function setupHostProvider(extensionContext: any, extensionDir: string, dataDir: string) {
|
||||
const createWebview = (_: WebviewProviderType): WebviewProvider => {
|
||||
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
@@ -52,8 +215,8 @@ function setupHostProvider() {
|
||||
log,
|
||||
getCallbackUrl,
|
||||
getBinaryLocation,
|
||||
EXTENSION_DIR,
|
||||
DATA_DIR,
|
||||
extensionDir,
|
||||
dataDir,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,7 +224,7 @@ function setupHostProvider() {
|
||||
* Sets up global error handlers to prevent the process from crashing
|
||||
* on unhandled exceptions and promise rejections
|
||||
*/
|
||||
function setupGlobalErrorHandlers() {
|
||||
function setupGlobalErrorHandlers(lockManager?: SqliteLockManager) {
|
||||
// Handle unhandled exceptions
|
||||
process.on("uncaughtException", (error: Error) => {
|
||||
log(`ERROR: Uncaught exception: ${error.message}`)
|
||||
@@ -86,15 +249,74 @@ function setupGlobalErrorHandlers() {
|
||||
// Graceful shutdown handlers
|
||||
process.on("SIGINT", () => {
|
||||
log("Received SIGINT, shutting down gracefully...")
|
||||
process.exit(0)
|
||||
shutdownGracefully(lockManager)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
log("Received SIGTERM, shutting down gracefully...")
|
||||
tearDown()
|
||||
|
||||
process.exit(0)
|
||||
shutdownGracefully(lockManager)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Request host bridge shutdown with retry logic and timeout handling.
|
||||
* Uses best-effort approach - logs failures but doesn't block shutdown.
|
||||
*/
|
||||
async function requestHostBridgeShutdown(): Promise<void> {
|
||||
try {
|
||||
await retryOperation(3, 2000, async () => {
|
||||
await HostProvider.env.shutdown({})
|
||||
})
|
||||
log("Host bridge shutdown requested successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to request host bridge shutdown: ${error}`)
|
||||
log("Proceeding with cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shutdown the cline-core process by:
|
||||
* 1. Calling shutdown RPC on the paired host bridge
|
||||
* 2. Cleaning up the lock manager entry
|
||||
* 3. Tearing down services
|
||||
* 4. Exiting the process
|
||||
*/
|
||||
async function shutdownGracefully(lockManager?: SqliteLockManager) {
|
||||
try {
|
||||
// Step 1: Tell the paired host bridge to shut down
|
||||
log("Requesting host bridge shutdown...")
|
||||
if (HostProvider.isInitialized()) {
|
||||
await requestHostBridgeShutdown()
|
||||
} else {
|
||||
log("Warning: HostProvider not initialized, cannot request shutdown")
|
||||
}
|
||||
|
||||
// Step 2: Clean up lock manager entry
|
||||
log("Cleaning up lock manager entry...")
|
||||
try {
|
||||
lockManager?.unregisterInstance()
|
||||
lockManager?.close()
|
||||
log("Lock manager entry cleaned up successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to clean up lock manager: ${error}`)
|
||||
}
|
||||
|
||||
// Step 3: Tear down services
|
||||
log("Tearing down services...")
|
||||
try {
|
||||
tearDown()
|
||||
log("Services torn down successfully")
|
||||
} catch (error) {
|
||||
log(`Warning: Failed to tear down services: ${error}`)
|
||||
}
|
||||
|
||||
log("Graceful shutdown completed")
|
||||
} catch (error) {
|
||||
log(`Error during graceful shutdown: ${error}`)
|
||||
} finally {
|
||||
// Step 4: Exit the process
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
|
||||
import { log } from "./utils"
|
||||
|
||||
const SERVING_STATUS = 1
|
||||
|
||||
interface PortCheckResult {
|
||||
canProceed: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface RegistryEntry {
|
||||
instanceAddress: string
|
||||
hostAddress: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a gRPC health client for the given address
|
||||
*/
|
||||
function createHealthClient(address: string): any {
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const grpcObj = grpc.loadPackageDefinition(healthDef) as unknown as any
|
||||
const Health = grpcObj.grpc.health.v1.Health
|
||||
return new Health(address, grpc.credentials.createInsecure())
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single health check on the given address
|
||||
*/
|
||||
async function checkHealthOnce(address: string): Promise<{ success: boolean; status?: number; error?: Error }> {
|
||||
const client = createHealthClient(address)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
resolve({ success: false, error: new Error("Health check timeout") })
|
||||
}, 5000) // 5 second timeout
|
||||
|
||||
client.check({ service: "" }, (err: unknown, resp: any) => {
|
||||
clearTimeout(timeout)
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
|
||||
if (err) {
|
||||
resolve({ success: false, error: err as Error })
|
||||
} else {
|
||||
resolve({ success: true, status: resp?.status })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to shut down a host bridge instance
|
||||
*/
|
||||
async function shutdownHostBridge(hostAddress: string): Promise<boolean> {
|
||||
try {
|
||||
log(`Attempting to shutdown host bridge at ${hostAddress}`)
|
||||
|
||||
// This would need to be implemented - we need a way to send shutdown to a specific host
|
||||
// For now, we'll just log that we would do this
|
||||
log(`Would send shutdown command to host bridge at ${hostAddress}`)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
log(`Failed to shutdown host bridge at ${hostAddress}: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a port is available for binding, following the registry-first approach
|
||||
*/
|
||||
export async function checkPortAvailability(port: number, lockManager: SqliteLockManager): Promise<PortCheckResult> {
|
||||
log(`Checking port availability for port ${port}`)
|
||||
|
||||
// Step 1: Check registry first
|
||||
const registryEntry = lockManager.getInstanceByPort(port)
|
||||
|
||||
if (!registryEntry) {
|
||||
log(`No registry entry found for port ${port}, free to bind`)
|
||||
return { canProceed: true }
|
||||
}
|
||||
|
||||
log(`Found registry entry for port ${port}: instance=${registryEntry.instanceAddress}, host=${registryEntry.hostAddress}`)
|
||||
|
||||
// Step 2: Perform health check on the registered instance
|
||||
const coreAddress = registryEntry.instanceAddress
|
||||
|
||||
const performHealthCheck = async (): Promise<{ success: boolean; status?: number; error?: Error }> => {
|
||||
return await checkHealthOnce(coreAddress)
|
||||
}
|
||||
|
||||
// First health check attempt
|
||||
let healthResult = await performHealthCheck()
|
||||
|
||||
if (!healthResult.success) {
|
||||
// Health check ERROR - not our process
|
||||
log(`Health check failed for ${coreAddress}: ${healthResult.error?.message}`)
|
||||
log(`This indicates a non-Cline process is using port ${port}`)
|
||||
|
||||
// Attempt to shutdown the registered host bridge
|
||||
const shutdownSuccess = await shutdownHostBridge(registryEntry.hostAddress)
|
||||
if (shutdownSuccess) {
|
||||
log(`Successfully requested shutdown of host bridge ${registryEntry.hostAddress}`)
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
|
||||
log(`Removed stale registry entry for ${registryEntry.instanceAddress}`)
|
||||
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Port ${port} is occupied by a non-Cline process. Registry has been cleaned up.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Health check succeeded - it's our process
|
||||
if (healthResult.status === SERVING_STATUS) {
|
||||
// Healthy Cline instance already running
|
||||
log(`Healthy Cline instance already running on port ${port}`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `A healthy Cline instance is already running on port ${port}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Health check succeeded but status is not SERVING - unhealthy Cline instance
|
||||
log(`Unhealthy Cline instance detected on port ${port} (status: ${healthResult.status}), retrying in 1 second`)
|
||||
|
||||
// Wait 1 second and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// Second health check attempt
|
||||
healthResult = await performHealthCheck()
|
||||
|
||||
if (!healthResult.success) {
|
||||
// Now it's erroring - something changed
|
||||
log(`Health check now failing after retry for ${coreAddress}: ${healthResult.error?.message}`)
|
||||
|
||||
// Clean up registry since the instance is no longer responding
|
||||
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
|
||||
log(`Removed non-responsive registry entry for ${registryEntry.instanceAddress}`)
|
||||
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Port ${port} had an unhealthy Cline instance that is now non-responsive. Registry cleaned up.`,
|
||||
}
|
||||
}
|
||||
|
||||
if (healthResult.status === SERVING_STATUS) {
|
||||
// Instance recovered
|
||||
log(`Cline instance on port ${port} has recovered and is now healthy`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Cline instance on port ${port} has recovered and is now serving`,
|
||||
}
|
||||
}
|
||||
|
||||
// Still unhealthy after retry
|
||||
log(`Cline instance on port ${port} remains unhealthy after retry (status: ${healthResult.status})`)
|
||||
return {
|
||||
canProceed: false,
|
||||
error: `Cline instance on port ${port} is unhealthy and did not recover after retry`,
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import { Controller } from "@core/controller"
|
||||
import { StreamingResponseHandler } from "@core/controller/grpc-handler"
|
||||
import { addProtobusServices } from "@generated/hosts/standalone/protobus-server-setup"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import { ReflectionService } from "@grpc/reflection"
|
||||
import { GrpcHandler, GrpcStreamingResponseHandler } from "@hosts/external/grpc-types"
|
||||
import * as health from "grpc-health-check"
|
||||
import { getPackageDefinition, log } from "./utils"
|
||||
|
||||
export const PROTOBUS_PORT = 26040
|
||||
export const DEFAULT_PROTOBUS_PORT = 26040
|
||||
export const DEFAULT_HOSTBRIDGE_PORT = 26041
|
||||
|
||||
export function startProtobusService(controller: Controller) {
|
||||
const server = new grpc.Server()
|
||||
@@ -28,7 +29,7 @@ export function startProtobusService(controller: Controller) {
|
||||
reflection.addToServer(server)
|
||||
|
||||
// Start the server.
|
||||
const host = process.env.PROTOBUS_ADDRESS || `127.0.0.1:${PROTOBUS_PORT}`
|
||||
const host = process.env.PROTOBUS_ADDRESS || `127.0.0.1:${DEFAULT_PROTOBUS_PORT}`
|
||||
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
|
||||
if (err) {
|
||||
log(`Could not start ProtoBus service: Failed to bind to ${host}, port may be unavailable. ${err.message}`)
|
||||
@@ -109,3 +110,46 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side health check for the hostbridge service (kept at bottom for clarity)
|
||||
const SERVING_STATUS = 1
|
||||
function createHealthClient(address?: string) {
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const grpcObj = grpc.loadPackageDefinition(healthDef) as unknown as any
|
||||
const Health = grpcObj.grpc.health.v1.Health
|
||||
const target = address || process.env.HOSTBRIDGE_ADDRESS || `localhost:${DEFAULT_HOSTBRIDGE_PORT}`
|
||||
return new Health(target, grpc.credentials.createInsecure())
|
||||
}
|
||||
|
||||
async function checkHealthOnce(client: any): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
client.check({ service: "" }, (err: unknown, resp: any) => {
|
||||
if (err) {
|
||||
return resolve(false)
|
||||
}
|
||||
return resolve(resp?.status === SERVING_STATUS)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForHostBridgeReady(timeoutMs = 60000, intervalMs = 500, address?: string): Promise<void> {
|
||||
const client = createHealthClient(address)
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const ok = await checkHealthOnce(client)
|
||||
if (ok) {
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
return
|
||||
}
|
||||
log("Waiting for hostbridge to be ready...")
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => setTimeout(r, intervalMs))
|
||||
}
|
||||
try {
|
||||
client.close?.()
|
||||
} catch {}
|
||||
throw new Error("HostBridge health check timed out")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as fs from "fs"
|
||||
import * as health from "grpc-health-check"
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
|
||||
// WE WILL HAVE TO MIGRATE THIS FROM DATA TO v1 LATER
|
||||
const SETTINGS_SUBFOLDER = "data"
|
||||
|
||||
const log = (...args: unknown[]) => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
@@ -51,4 +54,4 @@ async function asyncIteratorToCallbacks<T>(stream: AsyncIterable<T>, callbacks:
|
||||
}
|
||||
}
|
||||
|
||||
export { getPackageDefinition, log, asyncIteratorToCallbacks }
|
||||
export { getPackageDefinition, log, asyncIteratorToCallbacks, SETTINGS_SUBFOLDER }
|
||||
|
||||
@@ -8,62 +8,74 @@ import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { log } from "./utils"
|
||||
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
|
||||
|
||||
log("Running standalone cline", ExtensionRegistryInfo.version)
|
||||
log(`CLINE_ENVIRONMENT: ${process.env.CLINE_ENVIRONMENT}`)
|
||||
|
||||
export const CLINE_DIR = process.env.CLINE_DIR || `${os.homedir()}/.cline`
|
||||
export const DATA_DIR = path.join(CLINE_DIR, "data")
|
||||
const INSTALL_DIR = process.env.INSTALL_DIR || __dirname
|
||||
const WORKSPACE_STORAGE_DIR = process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
|
||||
|
||||
mkdirSync(DATA_DIR, { recursive: true })
|
||||
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
|
||||
log("Using settings dir:", DATA_DIR)
|
||||
|
||||
export const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
|
||||
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||
|
||||
const extension: Extension<void> = {
|
||||
id: ExtensionRegistryInfo.id,
|
||||
isActive: true,
|
||||
extensionPath: EXTENSION_DIR,
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
|
||||
exports: undefined, // There are no API exports in the standalone version.
|
||||
activate: async () => {},
|
||||
extensionKind: ExtensionKind.UI,
|
||||
function getPackageVersion(): string {
|
||||
// Use build-time injected version (only method)
|
||||
return process.env.CLINE_VERSION || "unknown" // todo: sarah wanted to change the way we get the extension version
|
||||
}
|
||||
|
||||
const extensionContext: ExtensionContext = {
|
||||
extension: extension,
|
||||
extensionMode: EXTENSION_MODE,
|
||||
const VERSION = getPackageVersion() // todo: sarah wanted to change the way we get the extension version
|
||||
log("Running standalone cline ", VERSION)
|
||||
|
||||
// Set up KV stores.
|
||||
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
|
||||
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
|
||||
log(`CLINE_ENVIRONMENT: ${process.env.CLINE_ENVIRONMENT}`)
|
||||
|
||||
// Set up URIs.
|
||||
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
|
||||
storagePath: WORKSPACE_STORAGE_DIR, // Deprecated, not used in cline.
|
||||
globalStorageUri: URI.file(DATA_DIR),
|
||||
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
|
||||
// Accept clineDir parameter, but fall back to env variable, then default
|
||||
function createExtensionContext(clineDir?: string): ExtensionContext {
|
||||
const CLINE_DIR = clineDir || process.env.CLINE_DIR || `${os.homedir()}/.cline`
|
||||
const DATA_DIR = path.join(CLINE_DIR, "data")
|
||||
const INSTALL_DIR = process.env.INSTALL_DIR || __dirname
|
||||
const WORKSPACE_STORAGE_DIR = process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
|
||||
|
||||
// Logs are global per extension, not per workspace.
|
||||
logUri: URI.file(DATA_DIR),
|
||||
logPath: DATA_DIR, // Deprecated, not used in cline.
|
||||
mkdirSync(DATA_DIR, { recursive: true })
|
||||
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
|
||||
log("Using settings dir:", DATA_DIR)
|
||||
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
|
||||
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
|
||||
const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
|
||||
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||
|
||||
subscriptions: [], // These need to be destroyed when the extension is deactivated.
|
||||
const extension: Extension<void> = {
|
||||
id: ExtensionRegistryInfo.id,
|
||||
isActive: true,
|
||||
extensionPath: EXTENSION_DIR,
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
|
||||
exports: undefined,
|
||||
activate: async () => {},
|
||||
extensionKind: ExtensionKind.UI,
|
||||
}
|
||||
|
||||
environmentVariableCollection: new EnvironmentVariableCollection(),
|
||||
const extensionContext: ExtensionContext = {
|
||||
extension: extension,
|
||||
extensionMode: EXTENSION_MODE,
|
||||
|
||||
// Workspace state is per project/workspace when WORKSPACE_STORAGE_DIR is provided by the host.
|
||||
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
|
||||
// Set up KV stores.
|
||||
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
|
||||
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
|
||||
|
||||
// Set up URIs.
|
||||
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
|
||||
storagePath: WORKSPACE_STORAGE_DIR,
|
||||
globalStorageUri: URI.file(DATA_DIR),
|
||||
globalStoragePath: DATA_DIR,
|
||||
|
||||
// Logs are global per extension, not per workspace.
|
||||
logUri: URI.file(DATA_DIR),
|
||||
logPath: DATA_DIR,
|
||||
|
||||
extensionUri: URI.file(EXTENSION_DIR),
|
||||
extensionPath: EXTENSION_DIR,
|
||||
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
|
||||
|
||||
subscriptions: [],
|
||||
|
||||
environmentVariableCollection: new EnvironmentVariableCollection(),
|
||||
|
||||
// Workspace state is per project/workspace when WORKSPACE_STORAGE_DIR is provided by the host.
|
||||
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
|
||||
}
|
||||
|
||||
return extensionContext
|
||||
}
|
||||
|
||||
console.log("Finished loading vscode context...")
|
||||
|
||||
export { extensionContext }
|
||||
export { createExtensionContext }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* TypeScript equivalent of the Go common.RetryOperation utility
|
||||
* Performs an operation with retry logic and timeout handling
|
||||
*/
|
||||
export async function retryOperation<T>(maxRetries: number, timeoutPerAttempt: number, operation: () => Promise<T>): Promise<T> {
|
||||
let lastError: Error | undefined
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Create a timeout promise
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Operation timeout")), timeoutPerAttempt),
|
||||
)
|
||||
|
||||
// Race the operation against timeout
|
||||
const result = await Promise.race([operation(), timeoutPromise])
|
||||
return result // Success - return result
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Brief delay before retry
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Operation failed after ${maxRetries} attempts: ${lastError?.message}`)
|
||||
}
|
||||
@@ -227,18 +227,18 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
)
|
||||
}, [selectedModelId])
|
||||
|
||||
// Check if the current model is Claude Sonnet 4.5 and determine the alternate variant
|
||||
const claudeSonnet45Variant = useMemo(() => {
|
||||
if (selectedModelId === "anthropic/claude-sonnet-4.5") {
|
||||
// Check if the current model is Claude Sonnet 4 and determine the alternate variant
|
||||
const claudeSonnet4Variant = useMemo(() => {
|
||||
if (selectedModelId === "anthropic/claude-sonnet-4") {
|
||||
return {
|
||||
current: "anthropic/claude-sonnet-4.5",
|
||||
alternate: "anthropic/claude-sonnet-4.5:1m",
|
||||
current: "anthropic/claude-sonnet-4",
|
||||
alternate: "anthropic/claude-sonnet-4:1m",
|
||||
linkText: "Switch to 1M context window model",
|
||||
}
|
||||
} else if (selectedModelId === "anthropic/claude-sonnet-4.5:1m") {
|
||||
} else if (selectedModelId === "anthropic/claude-sonnet-4:1m") {
|
||||
return {
|
||||
current: "anthropic/claude-sonnet-4.5:1m",
|
||||
alternate: "anthropic/claude-sonnet-4.5",
|
||||
current: "anthropic/claude-sonnet-4:1m",
|
||||
alternate: "anthropic/claude-sonnet-4",
|
||||
linkText: "Switch to 200K context window model",
|
||||
}
|
||||
}
|
||||
@@ -345,16 +345,16 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
)}
|
||||
</DropdownWrapper>
|
||||
|
||||
{claudeSonnet45Variant && (
|
||||
{claudeSonnet4Variant && (
|
||||
<div style={{ marginBottom: 2 }}>
|
||||
<VSCodeLink
|
||||
onClick={() => handleModelChange(claudeSonnet45Variant.alternate)}
|
||||
onClick={() => handleModelChange(claudeSonnet4Variant.alternate)}
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "10.5px",
|
||||
color: "var(--vscode-textLink-foreground)",
|
||||
}}>
|
||||
{claudeSonnet45Variant.linkText}
|
||||
{claudeSonnet4Variant.linkText}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,8 +15,26 @@ const THUMB_SIZE = 16
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
const LabelContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const Label = styled.label`
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-right: auto;
|
||||
`
|
||||
const Description = styled.p`
|
||||
font-size: 12px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
`
|
||||
|
||||
const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
|
||||
@@ -111,8 +129,7 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
|
||||
|
||||
const handleSliderChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value, 10)
|
||||
const clampedValue = Math.max(value, ANTHROPIC_MIN_THINKING_BUDGET)
|
||||
setLocalValue(clampedValue)
|
||||
setLocalValue(value)
|
||||
}, [])
|
||||
|
||||
const handleSliderComplete = () => {
|
||||
@@ -137,16 +154,21 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<VSCodeCheckbox checked={isEnabled} onClick={handleToggleChange}>
|
||||
Enable thinking{localValue && localValue > 0 ? ` (${localValue.toLocaleString()} tokens)` : ""}
|
||||
Enable extended thinking
|
||||
</VSCodeCheckbox>
|
||||
|
||||
{isEnabled && (
|
||||
<Container>
|
||||
<>
|
||||
<LabelContainer>
|
||||
<Label htmlFor="thinking-budget-slider">
|
||||
<strong>Budget:</strong> {localValue.toLocaleString()} tokens
|
||||
</Label>
|
||||
</LabelContainer>
|
||||
<RangeInput
|
||||
$max={maxSliderValue}
|
||||
$min={0}
|
||||
$min={ANTHROPIC_MIN_THINKING_BUDGET}
|
||||
$value={localValue}
|
||||
aria-describedby="thinking-budget-description"
|
||||
aria-label={`Thinking budget: ${localValue.toLocaleString()} tokens`}
|
||||
@@ -155,7 +177,7 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
|
||||
aria-valuenow={localValue}
|
||||
id="thinking-budget-slider"
|
||||
max={maxSliderValue}
|
||||
min={0}
|
||||
min={ANTHROPIC_MIN_THINKING_BUDGET}
|
||||
onChange={handleSliderChange}
|
||||
onMouseUp={handleSliderComplete}
|
||||
onTouchEnd={handleSliderComplete}
|
||||
@@ -163,9 +185,13 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
|
||||
type="range"
|
||||
value={localValue}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Description id="thinking-budget-description">
|
||||
Higher budgets may allow you to achieve more comprehensive and nuanced reasoning
|
||||
</Description>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { anthropicModels, CLAUDE_SONNET_1M_SUFFIX } from "@shared/api"
|
||||
import { anthropicModels, CLAUDE_SONNET_4_1M_SUFFIX } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useMemo } from "react"
|
||||
@@ -15,11 +15,10 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
|
||||
export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-sonnet-4-20250514",
|
||||
`claude-sonnet-4-20250514${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
`claude-sonnet-4-20250514${CLAUDE_SONNET_4_1M_SUFFIX}`,
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-5-20250929",
|
||||
`claude-sonnet-4-5-20250929${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -41,19 +40,19 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
|
||||
|
||||
// Check if the current model is Claude Sonnet 4.5 and determine the alternate variant
|
||||
const claudeSonnet45Variant = useMemo(() => {
|
||||
const SONNET_4_5_MODEL_ID = "claude-sonnet-4-5-20250929"
|
||||
if (selectedModelId === SONNET_4_5_MODEL_ID) {
|
||||
// Check if the current model is Claude Sonnet 4 and determine the alternate variant
|
||||
const claudeSonnet4Variant = useMemo(() => {
|
||||
const SONNET_4_MODEL_ID = "claude-sonnet-4-20250514"
|
||||
if (selectedModelId === SONNET_4_MODEL_ID) {
|
||||
return {
|
||||
current: SONNET_4_5_MODEL_ID,
|
||||
alternate: `${SONNET_4_5_MODEL_ID}${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
current: SONNET_4_MODEL_ID,
|
||||
alternate: `${SONNET_4_MODEL_ID}${CLAUDE_SONNET_4_1M_SUFFIX}`,
|
||||
linkText: "Switch to 1M context window model",
|
||||
}
|
||||
} else if (selectedModelId === `${SONNET_4_5_MODEL_ID}${CLAUDE_SONNET_1M_SUFFIX}`) {
|
||||
} else if (selectedModelId === `${SONNET_4_MODEL_ID}${CLAUDE_SONNET_4_1M_SUFFIX}`) {
|
||||
return {
|
||||
current: `${SONNET_4_5_MODEL_ID}${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
alternate: SONNET_4_5_MODEL_ID,
|
||||
current: `${SONNET_4_MODEL_ID}${CLAUDE_SONNET_4_1M_SUFFIX}`,
|
||||
alternate: SONNET_4_MODEL_ID,
|
||||
linkText: "Switch to 200K context window model",
|
||||
}
|
||||
}
|
||||
@@ -91,13 +90,13 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{claudeSonnet45Variant && (
|
||||
{claudeSonnet4Variant && (
|
||||
<div style={{ marginBottom: 2 }}>
|
||||
<VSCodeLink
|
||||
onClick={() =>
|
||||
handleModeFieldChange(
|
||||
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
|
||||
claudeSonnet45Variant.alternate,
|
||||
claudeSonnet4Variant.alternate,
|
||||
currentMode,
|
||||
)
|
||||
}
|
||||
@@ -106,7 +105,7 @@ export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: An
|
||||
fontSize: "10.5px",
|
||||
color: "var(--vscode-textLink-foreground)",
|
||||
}}>
|
||||
{claudeSonnet45Variant.linkText}
|
||||
{claudeSonnet4Variant.linkText}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_SUFFIX } from "@shared/api"
|
||||
import { bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_4_1M_SUFFIX } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
@@ -21,7 +21,7 @@ interface BedrockProviderProps {
|
||||
|
||||
export const BedrockProvider = ({ showModelOptions, isPopup, currentMode }: BedrockProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange, handleModeFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
|
||||
const { handleFieldChange, handleFieldsChange, handleModeFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
|
||||
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
@@ -108,7 +108,7 @@ export const BedrockProvider = ({ showModelOptions, isPopup, currentMode }: Bedr
|
||||
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
|
||||
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
|
||||
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption>
|
||||
<VSCodeOption value="us-west-1">us-west-1</VSCodeOption>
|
||||
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */}
|
||||
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
|
||||
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
|
||||
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
|
||||
@@ -305,8 +305,7 @@ export const BedrockProvider = ({ showModelOptions, isPopup, currentMode }: Bedr
|
||||
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
|
||||
selectedModelId === "anthropic.claude-sonnet-4-20250514-v1:0" ||
|
||||
selectedModelId === "anthropic.claude-sonnet-4-5-20250929-v1:0" ||
|
||||
selectedModelId === `anthropic.claude-sonnet-4-20250514-v1:0${CLAUDE_SONNET_1M_SUFFIX}` ||
|
||||
selectedModelId === `anthropic.claude-sonnet-4-5-20250929-v1:0${CLAUDE_SONNET_1M_SUFFIX}` ||
|
||||
selectedModelId === `anthropic.claude-sonnet-4-20250514-v1:0${CLAUDE_SONNET_4_1M_SUFFIX}` ||
|
||||
selectedModelId === "anthropic.claude-opus-4-1-20250805-v1:0" ||
|
||||
selectedModelId === "anthropic.claude-opus-4-20250514-v1:0" ||
|
||||
(modeFields.awsBedrockCustomSelected &&
|
||||
@@ -317,10 +316,7 @@ export const BedrockProvider = ({ showModelOptions, isPopup, currentMode }: Bedr
|
||||
modeFields.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-5-20250929-v1:0") ||
|
||||
(modeFields.awsBedrockCustomSelected &&
|
||||
modeFields.awsBedrockCustomModelBaseId ===
|
||||
`anthropic.claude-sonnet-4-20250514-v1:0${CLAUDE_SONNET_1M_SUFFIX}`) ||
|
||||
(modeFields.awsBedrockCustomSelected &&
|
||||
modeFields.awsBedrockCustomModelBaseId ===
|
||||
`anthropic.claude-sonnet-4-5-20250929-v1:0${CLAUDE_SONNET_1M_SUFFIX}`) ||
|
||||
`anthropic.claude-sonnet-4-20250514-v1:0${CLAUDE_SONNET_4_1M_SUFFIX}`) ||
|
||||
(modeFields.awsBedrockCustomSelected &&
|
||||
modeFields.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-1-20250805-v1:0") ||
|
||||
(modeFields.awsBedrockCustomSelected &&
|
||||
|
||||
@@ -102,7 +102,7 @@ export const VercelAIGatewayProvider = ({ showModelOptions, isPopup, currentMode
|
||||
<span>
|
||||
{" "}
|
||||
<a
|
||||
href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai"
|
||||
href="https://vercel.com/"
|
||||
style={{
|
||||
color: "var(--vscode-textLink-foreground)",
|
||||
textDecoration: "none",
|
||||
@@ -171,6 +171,16 @@ export const VercelAIGatewayProvider = ({ showModelOptions, isPopup, currentMode
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "15px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontStyle: "italic",
|
||||
}}>
|
||||
Note: Free tier users will see $0 costs as these requests are provided at no charge by Vercel AI Gateway.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user