mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a9510c1c | |||
| d03957f21d | |||
| a9bc4c7d67 | |||
| 737dce09d1 | |||
| a10d778bee | |||
| 58b0ea9afa | |||
| 90fa3d7336 | |||
| d0da22d5a9 | |||
| f2ce0b46a3 | |||
| 52ab767e44 | |||
| d858fb9360 | |||
| 428cdb670a | |||
| aab002fe65 | |||
| cc540d8158 | |||
| adc15c79d7 | |||
| af05d3497a | |||
| 23fa1cb481 | |||
| 800967d851 | |||
| 91deede3c3 | |||
| b11e6171ff | |||
| 42666d9ca7 | |||
| 07f944b668 | |||
| c6e5b1509c | |||
| 6787dedf47 | |||
| d07648746a | |||
| 07b49baa2a | |||
| 2668bcdbe0 | |||
| e5e293c32b | |||
| fea8313695 | |||
| 4b450f4488 | |||
| 2687ae149f | |||
| 3b93871b50 | |||
| 767b81b22b | |||
| 5db4970c7d | |||
| 4f66126a8c | |||
| 43006ca401 | |||
| 1064c631c5 | |||
| 981fe9cf09 | |||
| 90ab59d8f5 | |||
| f237dda413 | |||
| f08b0499d5 | |||
| 5c2d93617f | |||
| 99201f9944 | |||
| 0983a8a4b9 | |||
| 683096aed7 | |||
| 7189b224fc | |||
| fe3a75309d | |||
| 7a00be2d55 | |||
| 4e7af6eb5d | |||
| e9c8f67822 | |||
| bef7d2c75a | |||
| dfc660e73c | |||
| af0bc4dc6c | |||
| f37962bbf2 | |||
| e82597e069 | |||
| d1db8f747a | |||
| e8ba3c34fb | |||
| fadcd7e3ad | |||
| 81299d26d8 | |||
| 832025c8cc | |||
| f8a7a1cc58 | |||
| 3723288250 | |||
| 0b5e8f5c37 | |||
| a04f6050ee | |||
| ef7e8c5018 | |||
| 60b17d092b | |||
| 58808f3e2f | |||
| 281fd9505a | |||
| fe1da0fa6c | |||
| a505a79ec3 | |||
| ac41b0dd33 | |||
| fd3abdc68b | |||
| fe37ca9f29 | |||
| 7ecf395a9a |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Checkpoints multiroot pt.1: Accept array of workspaces when initializting checkpoints
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Run Testing platform within Test workflow
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
remove temperature settings in z.ai models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Interactive playwright script
|
||||
+121
-2
@@ -90,8 +90,14 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
- name: Unit Tests (with coverage on Linux)
|
||||
run: |
|
||||
if [ "${{ runner.os }}" = "Linux" ]; then
|
||||
npm install --no-save nyc
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
else
|
||||
npm run test:unit
|
||||
fi
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
@@ -125,6 +131,8 @@ jobs:
|
||||
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
|
||||
@@ -141,6 +149,69 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test-platform-integration:
|
||||
runs-on: ubuntu-latest
|
||||
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') }}
|
||||
|
||||
# Cache testing-platform dependencies
|
||||
- name: Cache testing-platform dependencies
|
||||
uses: actions/cache@v4
|
||||
id: testing-platform-cache
|
||||
with:
|
||||
path: testing-platform/node_modules
|
||||
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/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: Compile standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
|
||||
run: cd testing-platform && npm ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
continue-on-error: true
|
||||
timeout-minutes: 7
|
||||
# Temporarily wrapping the test command to always return a neutral exit code.
|
||||
# This prevents the job from showing as failed and avoids distracting developers
|
||||
# until the integration tests are ready to be enforced.
|
||||
run: |
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: coverage/**/lcov.info
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -219,3 +290,51 @@ jobs:
|
||||
--verbose
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
qlty:
|
||||
needs: [test, test-platform-integration]
|
||||
runs-on: ubuntu-latest
|
||||
# Run on PRs to main, pushes to main, and manual dispatches
|
||||
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
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: .
|
||||
|
||||
- name: Upload core unit tests coverage to Qlty
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
coverage-unit/lcov.info
|
||||
tag: unit:core
|
||||
|
||||
- name: Upload webview-ui unit tests coverage to Qlty
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
webview-ui/coverage/lcov.info
|
||||
tag: unit:webview-ui
|
||||
add-prefix: webview-ui/
|
||||
|
||||
- name: Download test platform integration core coverage artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: integration-core-coverage-reports
|
||||
|
||||
- name: Upload core integration tests coverage to Qlty
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
files: integration-core-coverage-reports/**/lcov.info
|
||||
tag: integration:core
|
||||
|
||||
@@ -20,6 +20,8 @@ webview-ui/src/**/*.js.map
|
||||
|
||||
# Ignore coverage directories and files
|
||||
coverage
|
||||
coverage-unit
|
||||
.nyc_output
|
||||
# But don't ignore the coverage scripts in .github/scripts/
|
||||
!.github/scripts/coverage/
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"all": true,
|
||||
"check-coverage": false,
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
|
||||
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/.nyc_output/**",
|
||||
"**/.vscode-test/**",
|
||||
"**/tests-results/**",
|
||||
"src/test/**",
|
||||
|
||||
"src/generated/**",
|
||||
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/build/**",
|
||||
"**/coverage/**",
|
||||
"**/coverage-unit/**",
|
||||
"**/proto/**",
|
||||
|
||||
"**/*.{config,setup}.{js,ts,mjs,cjs}",
|
||||
"**/vite-env.d.ts",
|
||||
|
||||
"**/*.{css,scss,sass,less,styl}",
|
||||
"**/*.{svg,png,jpg,jpeg,gif,ico}",
|
||||
"**/*.{json,yaml,yml}"
|
||||
],
|
||||
"extension": [
|
||||
".ts",
|
||||
".js"
|
||||
],
|
||||
"cache": true,
|
||||
"sourceMap": true,
|
||||
"instrument": true,
|
||||
"report-dir": "./coverage-unit"
|
||||
}
|
||||
@@ -64,3 +64,9 @@ old_docs/**
|
||||
e2e-build.mjs
|
||||
e2e.vsix
|
||||
test-results/
|
||||
|
||||
# Ignore Storybook files
|
||||
**/*.stories.tsx
|
||||
*storybook.log
|
||||
storybook-static
|
||||
**/StorybookDecorator.tsx
|
||||
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## [3.30.3]
|
||||
|
||||
- Add Oracle Code Assist provider
|
||||
|
||||
## [3.30.2]
|
||||
|
||||
- Fix UI tests
|
||||
|
||||
## [3.30.1]
|
||||
|
||||
- Fix model list not being updated in time for user to use shortcut button to update model to stealth model
|
||||
- Fix flicker issue when switching modes
|
||||
- Fix Sticky header in settings view overlaping with content on scroll
|
||||
- Add experimental yolo mode feature that disables all user approvals and automatically executes a task and navigates through plan to act mode until the task is complete
|
||||
|
||||
## [3.30.0]
|
||||
|
||||
- Add code-supernova stealth model
|
||||
|
||||
## [3.29.2]
|
||||
|
||||
- Fix: Reverted change that caused formatting issues
|
||||
- Fix: Moonshot - Pass max_tokens value to provider
|
||||
|
||||
## [3.29.1]
|
||||
|
||||
- Changeset bump + Announcement banner update
|
||||
|
||||
## [3.29.0]
|
||||
|
||||
- Updated Baseten provider to fetch models from server
|
||||
- Fix: Updated insufficient balance URL for easy Cline balance top-ups
|
||||
- Accessibility: Improvements to screen readers in MCP, Cline Rules, workflows, and history views.
|
||||
|
||||
## [3.28.4]
|
||||
|
||||
- Fix bug where some Windows machines had API request hanging
|
||||
|
||||
@@ -157,6 +157,36 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
- Update existing tests if your changes affect them
|
||||
- Include both unit tests and integration tests where appropriate
|
||||
|
||||
**End-to-End (E2E) Testing**
|
||||
|
||||
Cline includes comprehensive E2E tests using Playwright that simulate real user interactions with the extension in VS Code:
|
||||
|
||||
- **Running E2E tests:**
|
||||
```bash
|
||||
npm run test:e2e # Build and run all E2E tests
|
||||
npm run e2e # Run tests without rebuilding
|
||||
npm run test:e2e -- --debug # Run with interactive debugger
|
||||
```
|
||||
|
||||
- **Writing E2E tests:**
|
||||
- Tests are located in `src/test/e2e/`
|
||||
- Use the `e2e` fixture for single-root workspace tests
|
||||
- Use `e2eMultiRoot` fixture for multi-root workspace tests
|
||||
- Follow existing patterns in `auth.test.ts`, `chat.test.ts`, `diff.test.ts`, and `editor.test.ts`
|
||||
- See `src/test/e2e/README.md` for detailed documentation
|
||||
|
||||
- **Debug mode features:**
|
||||
- Interactive Playwright Inspector for step-by-step debugging
|
||||
- Record new interactions and generate test code automatically
|
||||
- Visual VS Code instance for manual testing
|
||||
- Element inspection and selector validation
|
||||
|
||||
- **Test environment:**
|
||||
- Automated VS Code setup with Cline extension loaded
|
||||
- Mock API server for backend testing
|
||||
- Temporary workspaces with test fixtures
|
||||
- Video recording for failed tests
|
||||
|
||||
4. **Version Management with Changesets**
|
||||
|
||||
- Create a changeset for any user-facing changes using `npm run changeset`
|
||||
|
||||
+2
-1
@@ -183,7 +183,8 @@
|
||||
"provider-config/openrouter",
|
||||
"provider-config/sap-aicore",
|
||||
"provider-config/vercel-ai-gateway",
|
||||
"provider-config/requesty"
|
||||
"provider-config/requesty",
|
||||
"provider-config/baseten"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Installing Cline for JetBrains (Early Access)"
|
||||
description: "Get early access to Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
|
||||
title: "Installing Cline for JetBrains"
|
||||
description: "Install Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
|
||||
---
|
||||
|
||||
<Frame>
|
||||
@@ -20,109 +20,103 @@ Cline for JetBrains works almost identically to Cline in VSCode. All the core fe
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
<Note>Cline for JetBrains is in early access. All core features are functional, with ongoing improvements based on user feedback.</Note>
|
||||
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
|
||||
|
||||
As part of our early access program, Cline for JetBrains is available through direct download before its official marketplace release. You'll need to install it manually from a downloaded file:
|
||||
**Method 1: Direct from your IDE**
|
||||
|
||||
### Manual Installation from Disk
|
||||
|
||||
1. **Download the Plugin:**
|
||||
- Go to [https://plugins.jetbrains.com/plugin/28247-cline/versions/eap](https://plugins.jetbrains.com/plugin/28247-cline/versions/eap)
|
||||
- Click **Download** to get the `.zip` file
|
||||
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-download.png"
|
||||
alt="JetBrains plugin marketplace showing Cline download page"
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
2. **Install from Disk:**
|
||||
- Open your JetBrains IDE
|
||||
- Go to **IntelliJ IDEA** (or whichever IDE you are in) → **Settings**
|
||||
**Method 2: Browser install**
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-settings.png"
|
||||
alt="JetBrains IDE settings dialog"
|
||||
/>
|
||||
</Frame>
|
||||
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.
|
||||
|
||||
- Select **Plugins** from the left sidebar
|
||||
- Click the gear icon ⚙️ and select **Install Plugin from Disk...**
|
||||
<details>
|
||||
<summary>Method 3: Manual installation</summary>
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-install-disk.png"
|
||||
alt="JetBrains IDE settings showing Install Plugin from Disk option"
|
||||
/>
|
||||
</Frame>
|
||||
Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline), then:
|
||||
|
||||
- Select the downloaded `.zip` file
|
||||
1. Go to Settings → Plugins
|
||||
2. Click the gear icon → Install Plugin from Disk
|
||||
3. Select the downloaded `.zip` file
|
||||
4. Restart your IDE
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-zip-file.png"
|
||||
alt="File selection dialog showing Cline plugin zip file"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
- Restart your IDE when prompted
|
||||
</details>
|
||||
|
||||
## Getting Started with Cline
|
||||
|
||||
After installation, you'll find Cline in your IDE:
|
||||
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**.
|
||||
|
||||
1. **Open Cline:**
|
||||
- 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.
|
||||
|
||||
2. **Sign In (optional, BYOK is also available):**
|
||||
- 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
|
||||
|
||||
3. **Start Coding:**
|
||||
- Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
|
||||
Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
|
||||
|
||||
## Key Differences from VSCode
|
||||
|
||||
While Cline for JetBrains includes all the same powerful features, there's one important difference to be aware of:
|
||||
|
||||
**Terminal Integration:** The terminal inside JetBrains isn't integrated with Cline the same way it is in VSCode. Cline can execute commands, but the output will only appear in the webview if you expand the **Command Output** section.
|
||||
|
||||
This means:
|
||||
- Commands still run successfully
|
||||
- You can see the output by clicking to expand Command Output in the chat
|
||||
- Terminal commands work the same way, just with a different display
|
||||
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:
|
||||
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.
|
||||
|
||||
- **Diff Editing:** Cline can read, write, and edit files with the same precision
|
||||
- **Tool Usage:** All of Cline's tools (file operations, web browsing, etc.) work identically
|
||||
- **API Providers:** Connect to Anthropic, OpenAI, local models, and more
|
||||
- **MCP Servers:** Full support for Model Context Protocol servers
|
||||
- **Cline Rules:** Custom instructions and workflows work the same way
|
||||
- **@ Mentions:** Reference files, folders, problems, and more
|
||||
- **Drag & Drop:** Add files and images to conversations
|
||||
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
|
||||
|
||||
- **Project Context:** Cline automatically understands your project structure, just like in VSCode
|
||||
- **Language Support:** Cline works with any language your JetBrains IDE supports
|
||||
- **Debugging Help:** Share error messages and stack traces directly in the chat
|
||||
- **Code Review:** Ask Cline to review your code changes before committing
|
||||
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
|
||||
- Restart your IDE completely (File → Exit and reopen)
|
||||
- Check **View** → **Tool Windows** → **Cline**
|
||||
- Ensure the plugin is enabled in **Settings** → **Plugins**
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
---
|
||||
title: "Installing Cline"
|
||||
description: "Cline is a VS Code extension that brings AI-powered coding assistance directly
|
||||
to your editor. Install using one of these methods:"
|
||||
description: "Cline brings AI-powered coding assistance to your editor. Available for VS Code and JetBrains IDEs."
|
||||
---
|
||||
|
||||
### Installation Options
|
||||
## Choose Your Editor
|
||||
|
||||
- **VS Code Marketplace (Recommended):** Fastest method for standard VS Code and Cursor users.
|
||||
- **Open VSX Registry:** For VS Code-compatible editors like VSCodium.
|
||||
Cline works across multiple development environments:
|
||||
|
||||
- **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
|
||||
|
||||
## VS Code Installation
|
||||
|
||||
### VS Code Marketplace: Step-by-Step Setup
|
||||
|
||||
@@ -46,6 +50,18 @@ For VS Code-compatible editors without Marketplace access (like VSCodium and Win
|
||||
4. Select "Cline" by saoudrizwan and click **Install**.
|
||||
5. Reload if prompted.
|
||||
|
||||
## JetBrains Installation
|
||||
|
||||
For IntelliJ IDEA, PyCharm, WebStorm, DataSpell, and other JetBrains IDEs:
|
||||
|
||||
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
|
||||
|
||||
**Need more help?** See our [complete JetBrains installation guide](/getting-started/installing-cline-jetbrains) for screenshots and troubleshooting.
|
||||
|
||||
### Creating Your Cline Account
|
||||
|
||||
Now that you have Cline installed, let's get you set up with your account:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "Baseten"
|
||||
description: "Learn how to configure and use Baseten's Model APIs with Cline. Access frontier open-source models with enterprise-grade performance, reliability, and competitive pricing."
|
||||
---
|
||||
|
||||
Baseten provides on-demand frontier model APIs designed for production applications, not just experimentation. Built on the Baseten Inference Stack, these APIs deliver enterprise-grade performance and reliability with optimized inference for leading open-source models from OpenAI, DeepSeek, Meta, Moonshot AI, and Alibaba Cloud.
|
||||
|
||||
**Website:** [https://www.baseten.co/products/model-apis/](https://www.baseten.co/products/model-apis/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to [Baseten](https://www.baseten.co/) and create an account or sign in.
|
||||
2. **Navigate to API Keys:** Access your dashboard and go to the API Keys section.
|
||||
3. **Create a Key:** Generate a new API key. Give it a descriptive name (e.g., "Cline").
|
||||
4. **Copy the Key:** Copy the API key immediately and store it securely.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports all current models under Baseten Model APIs, including:
|
||||
For the most updated pricing, please visit: https://www.baseten.co/products/model-apis/
|
||||
|
||||
**Reasoning Models:**
|
||||
- `deepseek-ai/DeepSeek-R1` - DeepSeek's first-generation reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-R1-0528` - Latest revision of DeepSeek's reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - \$0.50/\$1.50 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - \$0.77/\$0.77 per 1M tokens
|
||||
|
||||
**Flagship Models:**
|
||||
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
|
||||
- `moonshotai/Kimi-K2-Instruct` (Moonshot AI) - 1 trillion parameter model for agentic tasks (131K context) - \$0.60/\$2.50 per 1M tokens
|
||||
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
|
||||
|
||||
**Meta Llama 4 Series:**
|
||||
- `meta-llama/Llama-4-Maverick-17B-128E-Instruct` - High-efficiency processing (1M context!) - \$0.19/\$0.72 per 1M tokens
|
||||
- `meta-llama/Llama-4-Scout-17B-16E-Instruct` - Precise context understanding (1M context!) - \$0.13/\$0.50 per 1M tokens
|
||||
|
||||
**Coding Specialists:**
|
||||
- `Qwen/Qwen3-Coder-480B-A35B-Instruct`- Advanced coding and reasoning (262K context) - \$0.38/\$1.53 per 1M tokens
|
||||
- `Qwen/Qwen3-235B-A22B-Instruct-2507` - Math and reasoning expert (262K context) - \$0.22/\$0.80 per 1M tokens
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "Baseten" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Baseten API key into the "Baseten API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Production-First Architecture
|
||||
|
||||
Baseten's Model APIs are built for production environments with several key advantages:
|
||||
|
||||
#### Enterprise-Grade Reliability
|
||||
- **Four nines of uptime** (99.99%) through active-active redundancy
|
||||
- **Cloud-agnostic, multi-cluster autoscaling** for consistent availability
|
||||
- **SOC 2 Type II certified** and **HIPAA compliant** for security requirements
|
||||
|
||||
#### Optimized Performance
|
||||
- **Pre-optimized models** shipped with the Baseten Inference Stack
|
||||
- **Latest-generation GPUs** with multi-cloud infrastructure
|
||||
- **Ultra-fast inference** optimized from the bottom up for production workloads
|
||||
|
||||
#### Cost Efficiency
|
||||
- **5-10x less expensive** than closed alternatives
|
||||
- **Optimized multi-cloud infrastructure** for efficient resource utilization
|
||||
- **Transparent pricing** with no hidden costs or rate limit surprises
|
||||
|
||||
#### Developer Experience
|
||||
- **OpenAI compatible API** - migrate by swapping a single URL
|
||||
- **Drop-in replacement** for closed models with comprehensive observability
|
||||
- **Seamless scaling** from Model APIs to dedicated deployments
|
||||
|
||||
### Special Features
|
||||
|
||||
#### Function Calling & Tool Use
|
||||
All Baseten models support structured outputs, function calling, and tool use as part of the Baseten Inference Stack, making them ideal for agentic applications.
|
||||
|
||||
#### Reasoning Capabilities
|
||||
DeepSeek models offer enhanced reasoning with step-by-step thought processes, while maintaining production-ready performance.
|
||||
|
||||
#### Long Context Support
|
||||
- **Up to 1 million tokens** for Llama 4 models (Maverick and Scout)
|
||||
- **262K tokens** for Qwen3 models
|
||||
- **163K tokens** for DeepSeek models
|
||||
- **Perfect for code repositories** and complex multi-turn conversations
|
||||
|
||||
#### Quantization Optimizations
|
||||
Models are deployed with advanced quantization techniques (fp4, fp8, fp16) for optimal performance while maintaining quality.
|
||||
|
||||
### Migration from Other Providers
|
||||
|
||||
Baseten's OpenAI compatibility makes migration straightforward:
|
||||
|
||||
**From OpenAI:**
|
||||
- Swap `api.openai.com` with `inference.baseten.co/v1`
|
||||
- Keep existing request/response formats
|
||||
- Benefit from significant cost savings
|
||||
|
||||
**From Other Providers:**
|
||||
- Use standard OpenAI SDK format
|
||||
- Maintain existing prompting strategies
|
||||
- Access to newer open-source models
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Model Selection:** Choose models based on your specific use case - reasoning models for complex tasks, coding models for development work, and flagship models for general applications.
|
||||
- **Cost Optimization:** Baseten offers some of the most competitive pricing in the market, especially for open-source models.
|
||||
- **Context Windows:** Take advantage of large context windows (up to 1M tokens) for including substantial codebases and documentation.
|
||||
- **Enterprise Ready:** Baseten is designed for production use with enterprise-grade security, compliance, and reliability.
|
||||
- **Dynamic Model Updates:** Cline automatically fetches the latest model list from Baseten, ensuring access to new models as they're released.
|
||||
- **Multi-Cloud Capacity Management (MCM):** Baseten's multi-cloud infrastructure ensures high availability and low latency globally.
|
||||
- **Support:** Baseten provides dedicated support for production deployments and can work with you on dedicated resources as you scale.
|
||||
|
||||
### Pricing Information
|
||||
|
||||
Current pricing is highly competitive and transparent. For the most up-to-date pricing, visit the [Baseten Model APIs page](https://www.baseten.co/products/model-apis/). Prices typically range from \$0.10-\$6.00 per million tokens, making Baseten significantly more cost-effective than many closed-model alternatives while providing access to state-of-the-art open-source models.
|
||||
+10
-2
@@ -12,6 +12,9 @@ const standalone = process.argv.includes("--standalone")
|
||||
const e2eBuild = process.argv.includes("--e2e-build")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
// Read package.json to get version for build-time injection
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
@@ -160,9 +163,14 @@ const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These modules need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled. better-sqlite3 is a native module that also cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
|
||||
// Inject version at build time for standalone builds
|
||||
define: {
|
||||
...baseConfig.define,
|
||||
"process.env.CLINE_VERSION": JSON.stringify(packageJson.version),
|
||||
},
|
||||
}
|
||||
|
||||
// E2E build script configuration
|
||||
|
||||
Generated
+72
-22
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"chalk": "^4.1.2",
|
||||
"chalk": "5.6.2",
|
||||
"commander": "^9.4.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"execa": "^5.1.1",
|
||||
@@ -292,15 +292,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
@@ -607,9 +604,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -708,6 +706,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -843,6 +842,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -1003,6 +1018,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -1251,6 +1282,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
@@ -1695,13 +1727,9 @@
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.4",
|
||||
@@ -1909,9 +1937,9 @@
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -2054,6 +2082,17 @@
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
@@ -2164,6 +2203,17 @@
|
||||
"log-symbols": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"path-key": {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"chalk": "^4.1.2",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
|
||||
Generated
+412
-219
File diff suppressed because it is too large
Load Diff
+46
-8
@@ -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.28.4",
|
||||
"version": "3.30.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -360,11 +360,10 @@
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"install:e2e:extension": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
|
||||
"test:e2e": "playwright install && npm run install:e2e:extension && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:build": "playwright install && npm run install:e2e:extension && node src/test/e2e/utils/build.mjs",
|
||||
"test:e2e:optimal": "npm run install:e2e:extension && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "INTERACTIVE_E2E=true playwright test",
|
||||
"test:e2e:build": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
|
||||
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
@@ -405,8 +404,9 @@
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"chalk": "5.6.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
@@ -419,6 +419,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^2.6.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
@@ -452,6 +453,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
@@ -466,6 +468,7 @@
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
@@ -488,7 +491,7 @@
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -497,5 +500,40 @@
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"html"
|
||||
],
|
||||
"exclude": [
|
||||
"**/testing-platform/**",
|
||||
"**/webview-ui/**",
|
||||
"**/.vscode-test/**",
|
||||
"**/node_modules/**",
|
||||
"node_modules",
|
||||
"**/dist-standalone/src/**",
|
||||
"**/dist-standalone/vsce-extension/https:/**",
|
||||
"**/dist-standalone/vsce-extension/**",
|
||||
"**/dist-standalone/https:/**",
|
||||
"**/dist-standalone/LIB/src/**",
|
||||
"**/dist-standalone/pdfjs-dist/**",
|
||||
"**/*.d.ts",
|
||||
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/.nyc_output/**",
|
||||
"**/tests-results/**",
|
||||
"src/test/**",
|
||||
"**/src/xml/**",
|
||||
"**/standalone/**",
|
||||
"**/src/generated/**",
|
||||
"**/evals/cli/dist/**",
|
||||
"**/evals/cli/src/**",
|
||||
"dist"
|
||||
],
|
||||
"all": true,
|
||||
"exclude-after-remap": true
|
||||
}
|
||||
}
|
||||
|
||||
+1
-24
@@ -2,9 +2,8 @@ import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
const isInteractive = process?.env?.INTERACTIVE_E2E === "true"
|
||||
|
||||
const E2E_TEST_CONFIG = defineConfig({
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
forbidOnly: isCI,
|
||||
@@ -30,25 +29,3 @@ const E2E_TEST_CONFIG = defineConfig({
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const INTERACTIVE_UI_CONFIG = defineConfig({
|
||||
workers: 1,
|
||||
testDir: "src/test/e2e",
|
||||
testMatch: "interactive.ui.ts", // Different pattern to avoid running actual tests
|
||||
timeout: 0, // No timeout for interactive sessions
|
||||
fullyParallel: false,
|
||||
reporter: [["list"]],
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
},
|
||||
{
|
||||
name: "interactive-ui",
|
||||
testMatch: "interactive.ui.ts",
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default isInteractive ? INTERACTIVE_UI_CONFIG : E2E_TEST_CONFIG
|
||||
|
||||
@@ -38,6 +38,9 @@ service AccountService {
|
||||
rpc setUserOrganization(UserOrganizationUpdateRequest) returns (Empty);
|
||||
|
||||
rpc openrouterAuthClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Returns a link the webview can use to redirect back to the user's IDE.
|
||||
rpc getRedirectUrl(EmptyRequest) returns (String);
|
||||
}
|
||||
|
||||
message AuthStateChangedRequest {
|
||||
|
||||
@@ -33,6 +33,8 @@ service ModelsService {
|
||||
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Fetches available models from SAP AI Core
|
||||
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
|
||||
// Fetches available models from OCA
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -127,6 +129,48 @@ message UpdateApiConfigurationRequest {
|
||||
ModelsApiConfiguration api_configuration = 2;
|
||||
}
|
||||
|
||||
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
|
||||
message OcaModelInfo {
|
||||
// Maximum completion tokens per request supported by this model
|
||||
optional int64 max_tokens = 1;
|
||||
// Total context window in tokens (input + output)
|
||||
optional int64 context_window = 2;
|
||||
// Whether the model supports image inputs
|
||||
optional bool supports_images = 3;
|
||||
// Whether prompt caching is supported for this model
|
||||
bool supports_prompt_cache = 4;
|
||||
// Price per million input tokens (USD unless otherwise specified by provider)
|
||||
optional double input_price = 5;
|
||||
// Price per million output tokens (USD unless otherwise specified by provider)
|
||||
optional double output_price = 6;
|
||||
// Thinking/reasoning configuration if the model supports it
|
||||
optional ThinkingConfig thinking_config = 7;
|
||||
// Price per million tokens for prompt cache writes
|
||||
optional double cache_writes_price = 9;
|
||||
// Price per million tokens for prompt cache reads
|
||||
optional double cache_reads_price = 10;
|
||||
// Human-readable model description
|
||||
optional string description = 11;
|
||||
// Recommended default temperature for this model
|
||||
optional double temperature = 13;
|
||||
// Optional survey content to display in the UI
|
||||
optional string survey_content = 14;
|
||||
// Identifier for the survey associated with this model
|
||||
optional string survey_id = 15;
|
||||
// Optional banner content (e.g., deprecation or promotion notes)
|
||||
optional string banner = 16;
|
||||
// Canonical model identifier as reported by OCA
|
||||
string model_name = 17;
|
||||
}
|
||||
|
||||
// Aggregated OCA model catalog keyed by model identifier
|
||||
message OcaCompatibleModelInfo {
|
||||
// key: canonical model id as reported by OCA (e.g., "openai/gpt-4o-mini")
|
||||
// value: OcaModelInfo describing that model
|
||||
map<string, OcaModelInfo> models = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
// API Provider enumeration
|
||||
enum ApiProvider {
|
||||
ANTHROPIC = 0;
|
||||
@@ -164,6 +208,7 @@ enum ApiProvider {
|
||||
VERCEL_AI_GATEWAY = 32;
|
||||
QWEN_CODE = 33;
|
||||
DIFY = 34;
|
||||
OCA = 35;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -276,6 +321,9 @@ message ModelsApiConfiguration {
|
||||
optional string qwen_code_oauth_path = 70;
|
||||
optional string dify_api_key = 71;
|
||||
optional string dify_base_url = 72;
|
||||
optional string oca_base_url = 73;
|
||||
optional string oca_api_key = 74;
|
||||
optional string oca_refresh_token = 75;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -309,6 +357,8 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 129;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
optional string plan_mode_oca_model_id = 131;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
|
||||
|
||||
// Act mode configurations
|
||||
@@ -343,7 +393,6 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 229;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
optional string act_mode_oca_model_id = 231;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Service for account-related operations
|
||||
service OcaAccountService {
|
||||
// Handles the user clicking the login link in the UI.
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc ocaAccountLoginClicked(EmptyRequest) returns (String);
|
||||
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc ocaAccountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Subscribe to auth status update events (when authentication state changes)
|
||||
rpc ocaSubscribeToAuthStatusUpdate(EmptyRequest)
|
||||
returns (stream OcaAuthState);
|
||||
|
||||
}
|
||||
|
||||
|
||||
message OcaAuthState {
|
||||
optional OcaUserInfo user = 1;
|
||||
optional string api_key = 2;
|
||||
}
|
||||
|
||||
// User's information
|
||||
message OcaUserInfo {
|
||||
string uid = 1;
|
||||
optional string display_name = 2;
|
||||
optional string email = 3;
|
||||
}
|
||||
+9
-18
@@ -3,11 +3,13 @@ package cline;
|
||||
import "cline/common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
@@ -17,6 +19,7 @@ service StateService {
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
|
||||
}
|
||||
|
||||
message State {
|
||||
@@ -55,7 +58,6 @@ enum OpenaiReasoningEffort {
|
||||
LOW = 0;
|
||||
MEDIUM = 1;
|
||||
HIGH = 2;
|
||||
MINIMAL = 3;
|
||||
}
|
||||
|
||||
enum McpDisplayMode {
|
||||
@@ -106,16 +108,6 @@ message TelemetrySettingRequest {
|
||||
TelemetrySettingEnum setting = 2;
|
||||
}
|
||||
|
||||
// Browser settings for UpdateSettingsRequest
|
||||
message BrowserSettingsUpdate {
|
||||
optional Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
optional string custom_args = 6;
|
||||
}
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
@@ -136,8 +128,6 @@ message UpdateSettingsRequest {
|
||||
optional FocusChainSettings focus_chain_settings = 17;
|
||||
optional bool use_auto_condense = 18;
|
||||
optional string custom_prompt = 19;
|
||||
optional BrowserSettingsUpdate browser_settings = 20;
|
||||
optional string default_terminal_profile = 21;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -280,11 +270,12 @@ message FocusChainSettings {
|
||||
int32 remind_cline_interval = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message ProcessInfo {
|
||||
int32 process_id = 1;
|
||||
optional string version = 2;
|
||||
optional int64 uptime_ms = 3;
|
||||
}
|
||||
Binary file not shown.
@@ -20,10 +20,10 @@ service EnvService {
|
||||
// Returns the name and version of the host IDE or environment.
|
||||
rpc getHostVersion(cline.EmptyRequest) returns (GetHostVersionResponse);
|
||||
|
||||
// Returns the URI scheme for URIs that will redirect to the host environment.
|
||||
// e.g. vscode, idea, pycharm, etc. If the host does not support URIs it should
|
||||
// return an empty uriScheme.
|
||||
rpc getUriScheme(cline.EmptyRequest) returns (GetUriSchemeResponse);
|
||||
// Returns a URI that will redirect to the host environment.
|
||||
// e.g. vscode://saoudrizwan.claude-dev, idea://, pycharm://, etc.
|
||||
// If the host does not support URIs it should return empty.
|
||||
rpc getIdeRedirectUri(cline.EmptyRequest) returns (cline.String);
|
||||
|
||||
// Returns the telemetry settings of the host environment. This may return UNSUPPORTED
|
||||
// if the host does not specify telemetry settings for the plugin.
|
||||
@@ -52,7 +52,3 @@ message GetTelemetrySettingsResponse {
|
||||
message TelemetrySettingsEvent {
|
||||
Setting is_enabled = 1;
|
||||
}
|
||||
|
||||
message GetUriSchemeResponse {
|
||||
string uri_scheme = 1;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Interactive Playwright launcher for the Cline VS Code extension.
|
||||
*
|
||||
* Overview:
|
||||
* - Starts the mock Cline API server (from the e2e test fixtures).
|
||||
* - Downloads a stable build of VS Code (via @vscode/test-electron).
|
||||
* - Creates a temporary VS Code user profile directory.
|
||||
* - Installs and links the Cline extension (from dist/e2e.vsix and the dev path).
|
||||
* - Opens a test workspace and automatically reveals the Cline sidebar.
|
||||
* - Records **all gRPC calls** during the session for later inspection.
|
||||
* - Keeps VS Code running for manual interactive testing until the window is closed or Ctrl+C is pressed.
|
||||
* - Cleans up all resources (mock server, temp profile, Electron process) on exit.
|
||||
*
|
||||
* Usage:
|
||||
* 1. (Optional) Build and install the e2e extension:
|
||||
* npm run test:e2e:build
|
||||
*
|
||||
* 2. From the repo root, start the interactive session:
|
||||
* npm run test:playwright:interactive
|
||||
*
|
||||
* 3. VS Code will launch with the Cline extension loaded and gRPC recording enabled.
|
||||
*
|
||||
* 4. Interact with the extension manually.
|
||||
*
|
||||
* 5. Close the VS Code window or press Ctrl+C to end the session and trigger cleanup.
|
||||
*/
|
||||
|
||||
import { downloadAndUnzipVSCode, SilentReporter } from "@vscode/test-electron"
|
||||
import { mkdtempSync } from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { _electron } from "playwright"
|
||||
import { ClineApiServerMock } from "../src/test/e2e/fixtures/server"
|
||||
import { E2ETestHelper } from "../src/test/e2e/utils/helpers"
|
||||
|
||||
async function main() {
|
||||
await ClineApiServerMock.startGlobalServer()
|
||||
|
||||
const userDataDir = mkdtempSync(path.join(os.tmpdir(), "vsce-interactive"))
|
||||
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
|
||||
|
||||
// launch VSCode
|
||||
const app = await _electron.launch({
|
||||
executablePath,
|
||||
env: {
|
||||
...process.env,
|
||||
TEMP_PROFILE: "true",
|
||||
E2E_TEST: "true",
|
||||
CLINE_ENVIRONMENT: "local",
|
||||
GRPC_RECORDER_ENABLED: "true",
|
||||
GRPC_RECORDER_TESTS_FILTERS_ENABLED: "true",
|
||||
},
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-updates",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extensions",
|
||||
"--skip-welcome",
|
||||
"--skip-release-notes",
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
`--install-extension=${path.join(E2ETestHelper.CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`,
|
||||
`--extensionDevelopmentPath=${E2ETestHelper.CODEBASE_ROOT_DIR}`,
|
||||
path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "workspace"),
|
||||
],
|
||||
})
|
||||
|
||||
const page = await app.firstWindow()
|
||||
|
||||
await E2ETestHelper.openClineSidebar(page)
|
||||
|
||||
console.log("VSCode with Cline extension is now running!")
|
||||
console.log(`Temporary data directory on: ${userDataDir}`)
|
||||
console.log("You can manually interact with the extension.")
|
||||
console.log("Press Ctrl+C to close when done.")
|
||||
|
||||
async function teardown() {
|
||||
console.log("Cleaning up resources...")
|
||||
try {
|
||||
await app?.close()
|
||||
await ClineApiServerMock.stopGlobalServer?.()
|
||||
await E2ETestHelper.rmForRetries(userDataDir, { recursive: true })
|
||||
} catch (e) {
|
||||
console.log(`We could teardown interactive playwright properly, error:${e}`)
|
||||
}
|
||||
console.log("Finished cleaning up resources...")
|
||||
}
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
await teardown()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
await teardown()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
const win = await app.firstWindow()
|
||||
win.on("close", async () => {
|
||||
console.log("VS Code window closed.")
|
||||
await teardown()
|
||||
process.exit(0)
|
||||
})
|
||||
process.stdin.resume()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Failed to start:", err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -9,7 +9,7 @@ if [[ "${1:-}" == "-h" ]]; then
|
||||
fi
|
||||
|
||||
CORE_DIR=~/.cline/core
|
||||
INSTALL_DIR=$CORE_DIR/0.0.1
|
||||
INSTALL_DIR=$CORE_DIR/dev-instance/
|
||||
LOG_FILE=~/.cline/cline-core-service.log
|
||||
|
||||
ZIP_FILE=standalone.zip
|
||||
@@ -25,4 +25,6 @@ unp $ZIP_FILE > /dev/null
|
||||
|
||||
pkill -f cline-core.js || true
|
||||
|
||||
echo pwd: $(pwd)
|
||||
set -x
|
||||
NODE_PATH=./node_modules DEV_WORKSPACE_FOLDER=/tmp/ node cline-core.js 2>&1 | tee $LOG_FILE
|
||||
|
||||
@@ -28,19 +28,19 @@
|
||||
* Ideal for local development, testing, or lightweight E2E scenarios.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs"
|
||||
import { mkdtempSync, rmSync } from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import { ChildProcess, execSync, spawn } from "child_process"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { ClineApiServerMock } from "../src/test/e2e/fixtures/server/index"
|
||||
|
||||
// Configuration
|
||||
const PROTOBUS_PORT = process.env.PROTOBUS_PORT || "26040"
|
||||
const HOSTBRIDGE_PORT = process.env.HOSTBRIDGE_PORT || "26041"
|
||||
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd()
|
||||
const E2E_TEST = process.env.E2E_TEST || "true"
|
||||
const CLINE_ENVIRONMENT = process.env.CLINE_ENVIRONMENT || "local"
|
||||
const USE_C8 = process.env.USE_C8 === "true"
|
||||
|
||||
// Locate the standalone build directory and core file with flexible path resolution
|
||||
const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "..")
|
||||
@@ -48,8 +48,11 @@ const distDir = process.env.CLINE_DIST_DIR || path.join(projectRoot, "dist-stand
|
||||
const clineCoreFile = process.env.CLINE_CORE_FILE || "cline-core.js"
|
||||
const coreFile = path.join(distDir, clineCoreFile)
|
||||
|
||||
const childProcesses: ChildProcess[] = []
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("Starting Simple Cline gRPC Server...")
|
||||
console.log(`Project Root: ${projectRoot}`)
|
||||
console.log(`Workspace: ${WORKSPACE_DIR}`)
|
||||
console.log(`ProtoBus Port: ${PROTOBUS_PORT}`)
|
||||
console.log(`HostBridge Port: ${HOSTBRIDGE_PORT}`)
|
||||
@@ -75,27 +78,24 @@ async function main(): Promise<void> {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create temporary directories like e2e tests
|
||||
const extensionsDir = path.join(distDir, "vsce-extension")
|
||||
const userDataDir = mkdtempSync(path.join(os.tmpdir(), "vsce"))
|
||||
const extensionsDir = mkdtempSync(path.join(os.tmpdir(), "vsce"))
|
||||
const clineTestWorkspace = mkdtempSync(path.join(os.tmpdir(), "cline-test-workspace-"))
|
||||
|
||||
// Start hostbridge test server in background.
|
||||
// We run it as a child process to emulate how the extension currently operates
|
||||
console.log("Starting HostBridge test server...")
|
||||
const hostbridge: ChildProcess = spawn("npx", ["tsx", path.join(__dirname, "test-hostbridge-server.ts")], {
|
||||
stdio: "pipe",
|
||||
detached: false,
|
||||
env: {
|
||||
...process.env,
|
||||
TEST_HOSTBRIDGE_WORKSPACE_DIR: clineTestWorkspace,
|
||||
HOST_BRIDGE_ADDRESS: `127.0.0.1:${HOSTBRIDGE_PORT}`,
|
||||
},
|
||||
})
|
||||
childProcesses.push(hostbridge)
|
||||
|
||||
console.log(`Temp user data dir: ${userDataDir}`)
|
||||
console.log(`Temp extensions dir: ${extensionsDir}`)
|
||||
|
||||
// Extract standalone.zip to the extensions directory
|
||||
// Extract standalone.zip if needed
|
||||
const standaloneZipPath = path.join(distDir, "standalone.zip")
|
||||
if (!fs.existsSync(standaloneZipPath)) {
|
||||
console.error(`standalone.zip not found at: ${standaloneZipPath}`)
|
||||
@@ -104,47 +104,56 @@ async function main(): Promise<void> {
|
||||
|
||||
console.log("Extracting standalone.zip to extensions directory...")
|
||||
try {
|
||||
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
}
|
||||
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
|
||||
} catch (error) {
|
||||
console.error("Failed to extract standalone.zip:", error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Start the core service
|
||||
// We run it as a child process to emulate how the extension currently operates
|
||||
console.log("Starting Cline Core Service...")
|
||||
const coreService: ChildProcess = spawn("node", [clineCoreFile], {
|
||||
cwd: distDir,
|
||||
const covDir = path.join(projectRoot, `coverage/coverage-core-${PROTOBUS_PORT}`)
|
||||
|
||||
const baseArgs = ["--enable-source-maps", path.join(distDir, "cline-core.js")]
|
||||
|
||||
const spawnArgs = USE_C8 ? ["c8", "--report-dir", covDir, "node", ...baseArgs] : ["node", ...baseArgs]
|
||||
|
||||
console.log(`Starting Cline Core Service... (useC8=${USE_C8})`)
|
||||
|
||||
const coreService: ChildProcess = spawn("npx", spawnArgs, {
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: "./node_modules",
|
||||
DEV_WORKSPACE_FOLDER: WORKSPACE_DIR,
|
||||
PROTOBUS_ADDRESS: `127.0.0.1:${PROTOBUS_PORT}`,
|
||||
HOST_BRIDGE_ADDRESS: `localhost:${HOSTBRIDGE_PORT}`,
|
||||
E2E_TEST: E2E_TEST,
|
||||
CLINE_ENVIRONMENT: CLINE_ENVIRONMENT,
|
||||
E2E_TEST,
|
||||
CLINE_ENVIRONMENT,
|
||||
CLINE_DIR: userDataDir,
|
||||
INSTALL_DIR: extensionsDir,
|
||||
},
|
||||
stdio: "inherit",
|
||||
})
|
||||
childProcesses.push(coreService)
|
||||
|
||||
const shutdown = async () => {
|
||||
console.log("\nShutting down services...")
|
||||
|
||||
while (childProcesses.length > 0) {
|
||||
const child = childProcesses.pop()
|
||||
if (child && !child.killed) child.kill("SIGINT")
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
const shutdown = async (): Promise<void> => {
|
||||
console.log(`\n Shutting down services...\n${userDataDir}\n${extensionsDir}\n${clineTestWorkspace}\n`)
|
||||
hostbridge.kill()
|
||||
coreService.kill()
|
||||
await ClineApiServerMock.stopGlobalServer()
|
||||
|
||||
// Cleanup temp directories
|
||||
try {
|
||||
rmSync(userDataDir, { recursive: true, force: true })
|
||||
rmSync(extensionsDir, { recursive: true, force: true })
|
||||
rmSync(clineTestWorkspace, { recursive: true, force: true })
|
||||
console.log("Cleaned up temporary directories")
|
||||
} catch (error) {
|
||||
console.warn("Failed to cleanup temp directories:", error)
|
||||
} catch (err) {
|
||||
console.warn("Failed to cleanup temp directories:", err)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
@@ -155,24 +164,20 @@ async function main(): Promise<void> {
|
||||
|
||||
coreService.on("exit", (code) => {
|
||||
console.log(`Core service exited with code ${code}`)
|
||||
hostbridge.kill()
|
||||
process.exit(code || 0)
|
||||
shutdown()
|
||||
})
|
||||
|
||||
hostbridge.on("exit", (code) => {
|
||||
console.log(`HostBridge exited with code ${code}`)
|
||||
coreService.kill()
|
||||
process.exit(code || 0)
|
||||
shutdown()
|
||||
})
|
||||
|
||||
console.log("Cline gRPC Server is running!")
|
||||
console.log(`Connect to: 127.0.0.1:${PROTOBUS_PORT}`)
|
||||
console.log(`Cline gRPC Server is running on 127.0.0.1:${PROTOBUS_PORT}`)
|
||||
console.log("Press Ctrl+C to stop")
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error("Failed to start simple Cline server:", error)
|
||||
main().catch((err) => {
|
||||
console.error("Failed to start simple Cline server:", err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,61 +14,124 @@
|
||||
* Flags:
|
||||
* --server-logs Show server logs (hidden by default)
|
||||
* --count=<number> Repeat execution N times (default: 1)
|
||||
* --fix Automatically update spec files with actual responses
|
||||
* --coverage Generate integration test coverage information
|
||||
*
|
||||
* Environment Variables:
|
||||
* HOSTBRIDGE_PORT gRPC server port (default: 26040)
|
||||
* SERVER_BOOT_DELAY Server startup delay in ms (default: 3000)
|
||||
*/
|
||||
|
||||
import { ChildProcess, spawn } from "child_process"
|
||||
import fs from "fs"
|
||||
import minimist from "minimist"
|
||||
import net from "net"
|
||||
import path from "path"
|
||||
|
||||
const STANDALONE_GRPC_SERVER_PORT = process.env.STANDALONE_GRPC_SERVER_PORT || "26040"
|
||||
const SERVER_BOOT_DELAY = Number(process.env.SERVER_BOOT_DELAY) || 3000
|
||||
import kill from "tree-kill"
|
||||
|
||||
let showServerLogs = false
|
||||
let fix = false
|
||||
let coverage = false
|
||||
const WAIT_SERVER_DEFAULT_TIMEOUT = 15000
|
||||
const usedPorts = new Set<number>()
|
||||
|
||||
function startServer(): Promise<ChildProcess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = spawn("npx", ["tsx", "scripts/test-standalone-core-api-server.ts"], {
|
||||
stdio: showServerLogs ? "inherit" : "ignore",
|
||||
})
|
||||
|
||||
server.once("error", reject)
|
||||
|
||||
setTimeout(() => {
|
||||
if (server.killed) {
|
||||
reject(new Error("Server died during startup"))
|
||||
} else {
|
||||
resolve(server)
|
||||
/**
|
||||
* Find an available TCP port within the given range [min, max].
|
||||
*
|
||||
* - Ports are allocated sequentially (starting at `min`) rather than randomly,
|
||||
* which avoids accidental reuse when running hundreds of tests in a row.
|
||||
* - Each successfully allocated port is tracked in `usedPorts` to guarantee
|
||||
* it is never handed out again within the lifetime of this orchestrator.
|
||||
* - Before returning, the function binds a temporary server to the port to
|
||||
* verify that the OS really considers it available, then immediately closes it.
|
||||
*
|
||||
* This approach makes the orchestrator much more robust on CI (e.g. GitHub Actions),
|
||||
* where a just-terminated server may leave its socket in TIME_WAIT and cause
|
||||
* flakiness if the same port is reallocated too soon.
|
||||
*/
|
||||
async function getAvailablePort(min = 20000, max = 49151): Promise<number> {
|
||||
return new Promise((resolve, _) => {
|
||||
const tryPort = (candidate?: number) => {
|
||||
const port = candidate ?? Math.floor(Math.random() * (max - min + 1)) + min
|
||||
if (usedPorts.has(port)) {
|
||||
// already allocated in this run
|
||||
return tryPort()
|
||||
}
|
||||
}, SERVER_BOOT_DELAY)
|
||||
const server = net.createServer()
|
||||
server.once("error", () => tryPort())
|
||||
server.once("listening", () => {
|
||||
server.close(() => {
|
||||
usedPorts.add(port) // mark reserved
|
||||
resolve(port)
|
||||
})
|
||||
})
|
||||
server.listen(port, "127.0.0.1")
|
||||
}
|
||||
tryPort()
|
||||
})
|
||||
}
|
||||
|
||||
// Poll until a given TCP port on a host is accepting connections.
|
||||
async function waitForPort(port: number, host = "127.0.0.1", timeout = 10000): Promise<void> {
|
||||
const start = Date.now()
|
||||
const waitForPortSleepMs = 100
|
||||
while (Date.now() - start < timeout) {
|
||||
await new Promise((res) => setTimeout(res, waitForPortSleepMs))
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const socket = net.connect(port, host, () => {
|
||||
socket.destroy()
|
||||
resolve()
|
||||
})
|
||||
socket.on("error", reject)
|
||||
})
|
||||
return
|
||||
} catch {
|
||||
// try again
|
||||
}
|
||||
}
|
||||
throw new Error(`Timeout waiting for ${host}:${port}`)
|
||||
}
|
||||
|
||||
async function startServer(): Promise<{ server: ChildProcess; grpcPort: string }> {
|
||||
const grpcPort = (await getAvailablePort()).toString()
|
||||
const hostbridgePort = (await getAvailablePort()).toString()
|
||||
|
||||
const server = spawn("npx", ["tsx", "scripts/test-standalone-core-api-server.ts"], {
|
||||
stdio: showServerLogs ? "inherit" : "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
PROTOBUS_PORT: grpcPort,
|
||||
HOSTBRIDGE_PORT: hostbridgePort,
|
||||
USE_C8: coverage ? "true" : "false",
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for either the server to become ready or fail on spawn error
|
||||
await Promise.race([
|
||||
waitForPort(Number(grpcPort), "127.0.0.1", WAIT_SERVER_DEFAULT_TIMEOUT),
|
||||
new Promise((_, reject) => server.once("error", reject)),
|
||||
])
|
||||
|
||||
return { server, grpcPort }
|
||||
}
|
||||
|
||||
function stopServer(server: ChildProcess): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
server.once("exit", () => resolve())
|
||||
server.kill("SIGINT")
|
||||
setTimeout(() => {
|
||||
if (!server.killed) {
|
||||
server.kill("SIGKILL")
|
||||
resolve()
|
||||
}
|
||||
}, 5000)
|
||||
if (!server.pid) return resolve()
|
||||
|
||||
kill(server.pid, "SIGINT", (err) => {
|
||||
if (err) console.warn("Failed to kill server process:", err)
|
||||
server.once("exit", () => resolve())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function runTestingPlatform(specFile: string): Promise<void> {
|
||||
function runTestingPlatform(specFile: string, grpcPort: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const testProcess = spawn("npx", ["ts-node", "index.ts", specFile], {
|
||||
const testProcess = spawn("npx", ["ts-node", "index.ts", specFile, ...(fix ? ["--fix"] : [])], {
|
||||
cwd: path.join(process.cwd(), "testing-platform"),
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
HOSTBRIDGE_PORT: STANDALONE_GRPC_SERVER_PORT,
|
||||
STANDALONE_GRPC_SERVER_PORT: grpcPort,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -80,9 +143,9 @@ function runTestingPlatform(specFile: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function runSpec(specFile: string): Promise<void> {
|
||||
const server = await startServer()
|
||||
const { server, grpcPort } = await startServer()
|
||||
try {
|
||||
await runTestingPlatform(specFile)
|
||||
await runTestingPlatform(specFile, grpcPort)
|
||||
console.log(`✅ ${path.basename(specFile)} passed`)
|
||||
} finally {
|
||||
await stopServer(server)
|
||||
@@ -133,8 +196,7 @@ async function runAll(inputPath: string, count: number) {
|
||||
console.log(`✅ Passed: ${success}`)
|
||||
if (failure > 0) console.log(`❌ Failed: ${failure}`)
|
||||
console.log(`📋 Total specs: ${specFiles.length} Total runs: ${specFiles.length * count}`)
|
||||
const totalElapsed = ((Date.now() - totalStart) / 1000).toFixed(2)
|
||||
console.log(`\n🏁 All runs completed in ${totalElapsed}s`)
|
||||
console.log(`🏁 All runs completed in ${((Date.now() - totalStart) / 1000).toFixed(2)}s`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -142,9 +204,13 @@ async function main() {
|
||||
const inputPath = args._[0]
|
||||
const count = Number(args.count)
|
||||
showServerLogs = Boolean(args["server-logs"])
|
||||
fix = Boolean(args["fix"])
|
||||
coverage = Boolean(args["coverage"])
|
||||
|
||||
if (!inputPath) {
|
||||
console.error("Usage: npx tsx scripts/testing-platform-orchestrator.ts <spec-file-or-folder> [--count=N] [--server-logs]")
|
||||
console.error(
|
||||
"Usage: npx tsx scripts/testing-platform-orchestrator.ts <spec-file-or-folder> [--count=N] [--server-logs] [--fix] [--coverage]",
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandlerOptions, ModelInfo } from "@shared/api"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from "../../core/api/index"
|
||||
import { ApiStream } from "../../core/api/transform/stream"
|
||||
|
||||
interface DifyHandlerOptions {
|
||||
difyApiKey?: string
|
||||
difyBaseUrl?: string
|
||||
}
|
||||
|
||||
export class DifyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private options: DifyHandlerOptions
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
private conversationId: string | null = null
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
constructor(options: DifyHandlerOptions) {
|
||||
this.options = options
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { ErrorService } from "./services/error"
|
||||
import { featureFlagsService } from "./services/feature-flags"
|
||||
import { initializeDistinctId } from "./services/logging/distinctId"
|
||||
@@ -62,7 +63,7 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
// Version checking for autoupdate notification
|
||||
const currentVersion = context.extension.packageJSON.version
|
||||
const currentVersion = ExtensionRegistryInfo.version
|
||||
const previousVersion = context.globalState.get<string>("clineVersion")
|
||||
// Perform post-update actions if necessary
|
||||
try {
|
||||
@@ -71,7 +72,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
|
||||
// Use the same condition as announcements: focus when there's a new announcement to show
|
||||
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
|
||||
const latestAnnouncementId = getLatestAnnouncementId(context)
|
||||
const latestAnnouncementId = getLatestAnnouncementId()
|
||||
|
||||
if (lastShownAnnouncementId !== latestAnnouncementId) {
|
||||
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
|
||||
|
||||
+14
-4
@@ -21,6 +21,7 @@ import { LmStudioHandler } from "./providers/lmstudio"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { NebiusHandler } from "./providers/nebius"
|
||||
import { OcaHandler } from "./providers/oca"
|
||||
import { OllamaHandler } from "./providers/ollama"
|
||||
import { OpenAiHandler } from "./providers/openai"
|
||||
import { OpenAiNativeHandler } from "./providers/openai-native"
|
||||
@@ -352,10 +353,6 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
case "dify": // Add Dify.ai handler
|
||||
console.log("[DIFY DEBUG] Instantiating DifyHandler with options:", {
|
||||
difyApiKeyPresent: !!options.difyApiKey,
|
||||
difyBaseUrl: options.difyBaseUrl,
|
||||
})
|
||||
return new DifyHandler({
|
||||
difyApiKey: options.difyApiKey,
|
||||
difyBaseUrl: options.difyBaseUrl,
|
||||
@@ -376,6 +373,19 @@ function createHandlerForProvider(
|
||||
zaiApiKey: options.zaiApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "oca":
|
||||
return new OcaHandler({
|
||||
ocaBaseUrl: options.ocaBaseUrl,
|
||||
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
|
||||
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
ocaUsePromptCache:
|
||||
mode === "plan"
|
||||
? options.planModeOcaModelInfo?.supportsPromptCache
|
||||
: options.actModeOcaModelInfo?.supportsPromptCache,
|
||||
taskId: options.ulid,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "should"
|
||||
import { ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
|
||||
import { ApiHandlerOptions } from "@shared/api"
|
||||
import { Readable } from "stream"
|
||||
import type { AwsBedrockHandlerOptions } from "../bedrock"
|
||||
import { AwsBedrockHandler } from "../bedrock"
|
||||
|
||||
describe("AwsBedrockHandler", () => {
|
||||
@@ -202,8 +202,8 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
const mockOptions: AwsBedrockHandlerOptions = {
|
||||
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
awsAccessKey: "test-key",
|
||||
awsSecretKey: "test-secret",
|
||||
@@ -214,9 +214,9 @@ describe("AwsBedrockHandler", () => {
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
actModeAwsBedrockCustomSelected: false,
|
||||
actModeAwsBedrockCustomModelBaseId: undefined,
|
||||
actModeThinkingBudgetTokens: 1600,
|
||||
awsBedrockCustomSelected: false,
|
||||
awsBedrockCustomModelBaseId: undefined,
|
||||
thinkingBudgetTokens: 1600,
|
||||
}
|
||||
|
||||
const mockModelInfo = {
|
||||
|
||||
@@ -146,20 +146,15 @@ export class BasetenHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports vision/images
|
||||
*/
|
||||
supportsImages(): boolean {
|
||||
const model = this.getModel()
|
||||
return model.info.supportsImages === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports tools
|
||||
*/
|
||||
supportsTools(): boolean {
|
||||
const _model = this.getModel()
|
||||
// Baseten models support tools via OpenAI-compatible API
|
||||
return true
|
||||
const model = this.getModel()
|
||||
const modelInfo = model.info as any
|
||||
|
||||
// Use dynamic API data when available, fallback to true since all current Baseten models support tools
|
||||
// (as of 2025-09-16 - could change if Baseten add non-tool models in future, currently no plans to do so)
|
||||
return modelInfo.supportedFeatures ? modelInfo.supportedFeatures.includes("tools") : true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
|
||||
export interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
@@ -30,7 +30,7 @@ interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
|
||||
awsProfile?: string
|
||||
awsBedrockEndpoint?: string
|
||||
awsBedrockCustomSelected?: boolean
|
||||
awsBedrockCustomModelBaseId?: BedrockModelId
|
||||
awsBedrockCustomModelBaseId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
if (baseModel && baseModel in bedrockModels) {
|
||||
return {
|
||||
id: modelId,
|
||||
info: bedrockModels[baseModel],
|
||||
info: bedrockModels[baseModel as BedrockModelId],
|
||||
}
|
||||
}
|
||||
// For custom models without valid base model in bedrock model list, use default model's capabilities
|
||||
|
||||
@@ -162,7 +162,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (this.getModel().id === "cline/sonic") {
|
||||
if (this.getModel().id === "cline/code-supernova") {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface DifyHandlerOptions {
|
||||
difyApiKey?: string
|
||||
difyBaseUrl?: string
|
||||
}
|
||||
|
||||
// Dify API Response Types
|
||||
export interface DifyFileResponse {
|
||||
id: string
|
||||
@@ -66,14 +71,14 @@ interface DifyConversationResponse {
|
||||
}
|
||||
|
||||
export class DifyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private options: DifyHandlerOptions
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
private conversationId: string | null = null
|
||||
private currentTaskId: string | null = null
|
||||
private abortController: AbortController | null = null
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
constructor(options: DifyHandlerOptions) {
|
||||
this.options = options
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
@@ -49,6 +49,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { APIError, OpenAIError } from "openai"
|
||||
import type { FinalRequestOptions, Headers as OpenAIHeaders } from "openai/core"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { DEFAULT_OCA_BASE_URL, OCI_HEADER_OPC_REQUEST_ID } from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ocaBaseUrl?: string
|
||||
ocaModelId?: string
|
||||
ocaModelInfo?: LiteLLMModelInfo
|
||||
thinkingBudgetTokens?: number
|
||||
ocaUsePromptCache?: boolean
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
export class OcaHandler implements ApiHandler {
|
||||
protected options: OcaHandlerOptions
|
||||
protected client: OpenAI | undefined
|
||||
|
||||
constructor(options: OcaHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected initializeClient(options: OcaHandlerOptions) {
|
||||
return new (class OCIOpenAI extends OpenAI {
|
||||
protected override async prepareOptions(opts: FinalRequestOptions<unknown>): Promise<void> {
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
opts.headers ??= {}
|
||||
// OCA Headers
|
||||
const ociHeaders = await createOcaHeaders(token, options.taskId!)
|
||||
opts.headers = { ...opts.headers, ...ociHeaders }
|
||||
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
|
||||
return super.prepareOptions(opts)
|
||||
}
|
||||
|
||||
protected override makeStatusError(
|
||||
status: number | undefined,
|
||||
error: Object | undefined,
|
||||
message: string | undefined,
|
||||
headers: OpenAIHeaders | undefined,
|
||||
): APIError {
|
||||
interface OciError {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
let ociErrorMessage = message
|
||||
if (typeof error === "object" && error !== null) {
|
||||
try {
|
||||
ociErrorMessage = JSON.stringify(error)
|
||||
const ociErr = error as OciError
|
||||
if (ociErr.code !== undefined && ociErr.message !== undefined) {
|
||||
ociErrorMessage = `${ociErr.code}: ${ociErr.message}`
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const opcRequestId = headers?.[OCI_HEADER_OPC_REQUEST_ID]
|
||||
if (opcRequestId) {
|
||||
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
|
||||
}
|
||||
return super.makeStatusError(status, error, ociErrorMessage, headers)
|
||||
}
|
||||
})({
|
||||
baseURL: options.ocaBaseUrl || DEFAULT_OCA_BASE_URL,
|
||||
apiKey: "noop",
|
||||
})
|
||||
}
|
||||
|
||||
protected ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.ocaModelId) {
|
||||
throw new Error("Oracle Code Assist (OCA) model is not selected")
|
||||
}
|
||||
try {
|
||||
this.client = this.initializeClient(this.options)
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
|
||||
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
headers: ociHeaders,
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
usage: {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
}
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini")
|
||||
|
||||
// Configuration for extended thinking
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0
|
||||
const maxTokens: number | undefined = this.options.ocaModelInfo?.maxTokens
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
}
|
||||
|
||||
// Define cache control object if prompt caching is enabled
|
||||
const cacheControl = this.options.ocaUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined
|
||||
|
||||
// Add cache_control to system message if enabled
|
||||
const enhancedSystemMessage = {
|
||||
...systemMessage,
|
||||
...(cacheControl && cacheControl),
|
||||
}
|
||||
|
||||
// Find the last two user messages to apply caching
|
||||
const userMsgIndices = formattedMessages.reduce((acc, msg, index) => {
|
||||
if (msg.role === "user") {
|
||||
acc.push(index)
|
||||
}
|
||||
return acc
|
||||
}, [] as number[])
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
// Apply cache_control to the last two user messages if enabled
|
||||
const enhancedMessages = formattedMessages.map((message, index) => {
|
||||
if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && cacheControl) {
|
||||
return {
|
||||
...message,
|
||||
...cacheControl,
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
temperature,
|
||||
stream: true,
|
||||
max_completion_tokens: maxTokens,
|
||||
max_tokens: maxTokens,
|
||||
stream_options: { include_usage: true },
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
...(this.options.taskId && {
|
||||
litellm_session_id: `cline-${this.options.taskId}`,
|
||||
}), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
const outputCost = (await this.calculateCost(0, 1e6)) || 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning events (thinking)
|
||||
// Thinking is not in the standard types but may be in the response
|
||||
interface ThinkingDelta {
|
||||
thinking?: string
|
||||
}
|
||||
|
||||
if ((delta as ThinkingDelta)?.thinking) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta as ThinkingDelta).thinking || "",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
const usage = chunk.usage as {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
cache_creation_input_tokens?: number
|
||||
prompt_cache_miss_tokens?: number
|
||||
cache_read_input_tokens?: number
|
||||
prompt_cache_hit_tokens?: number
|
||||
}
|
||||
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
|
||||
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
if (this.options.sapAiCoreUseOrchestrationMode ?? true) {
|
||||
if (this.options.sapAiCoreUseOrchestrationMode) {
|
||||
yield* this.createMessageWithOrchestration(systemPrompt, messages)
|
||||
} else {
|
||||
yield* this.createMessageWithDeployments(systemPrompt, messages)
|
||||
|
||||
@@ -85,7 +85,6 @@ export class ZAiHandler implements ApiHandler {
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
|
||||
@@ -139,10 +139,6 @@ export async function createOpenRouterStream(
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
}
|
||||
break
|
||||
case "cline/sonic":
|
||||
temperature = 0.7
|
||||
topP = 0.95
|
||||
break
|
||||
default:
|
||||
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
|
||||
@@ -38,6 +38,7 @@ export const toolParamNames = [
|
||||
"additional_context",
|
||||
"needs_more_exploration",
|
||||
"task_progress",
|
||||
"timeout",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("FileContextTracker", () => {
|
||||
|
||||
// Mock controller and context
|
||||
mockController = {
|
||||
context: { globalStorageUri: { fsPath: "/mock/storage" } } as vscode.ExtensionContext,
|
||||
context: {} as vscode.ExtensionContext,
|
||||
} as unknown as Controller
|
||||
|
||||
// Mock disk module functions
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function refreshClineRulesToggles(
|
||||
localToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global toggles
|
||||
const globalClineRulesToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalClineRulesToggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
|
||||
|
||||
@@ -247,11 +247,11 @@ export async function deleteRuleFile(
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
if (type === "workflow") {
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
} else {
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function refreshWorkflowToggles(
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global workflows
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { EmptyRequest, String } from "@shared/proto/cline/common"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Constructs and returns a URL that will redirect to the user's IDE.
|
||||
*/
|
||||
export async function getRedirectUrl(_controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
const url = (await HostProvider.env.getIdeRedirectUri({})).value
|
||||
return { value: url }
|
||||
}
|
||||
@@ -19,8 +19,7 @@ export async function discoverBrowser(controller: Controller, _request: EmptyReq
|
||||
// This way we don't override the user's preference
|
||||
|
||||
// Test the connection to get the endpoint
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
return BrowserConnection.create({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Controller } from "../index"
|
||||
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
try {
|
||||
// Get browser settings from extension state
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
|
||||
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
|
||||
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
|
||||
|
||||
@@ -11,8 +11,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return ChromePath.create({
|
||||
|
||||
@@ -10,8 +10,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
|
||||
try {
|
||||
const { browserSettings } = await controller.getStateToPostToWebview()
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
|
||||
// Relaunch Chrome in debug mode
|
||||
await browserSession.relaunchChromeDebugMode(controller)
|
||||
|
||||
@@ -12,8 +12,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
|
||||
try {
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const browserSession = new BrowserSession(controller.context, controller.stateManager)
|
||||
const text = request.value || ""
|
||||
|
||||
// If no text is provided, try auto-discovery
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Controller } from ".."
|
||||
/**
|
||||
* Opens a file in the editor
|
||||
@@ -8,8 +9,8 @@ import { Controller } from ".."
|
||||
* @param request The request message containing the file path in the 'value' field
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openTaskHistory(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
const globalStoragePath = controller.context.globalStorageUri.fsPath
|
||||
export async function openTaskHistory(_controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
const globalStoragePath = HostProvider.get().globalStorageFsPath
|
||||
const taskHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
|
||||
if (request.value) {
|
||||
openFileIntegration(taskHistoryPath)
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
|
||||
// This is the same core logic as in the original handler
|
||||
if (isGlobal) {
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
toggles[rulePath] = enabled
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
}
|
||||
|
||||
// Get the current state to return in the response
|
||||
const globalToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
|
||||
return ToggleClineRules.create({
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
|
||||
// Update the toggles based on isGlobal flag
|
||||
if (isGlobal) {
|
||||
// Global workflows
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
import { Controller } from "@core/controller"
|
||||
import { IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
|
||||
import { GrpcRecorderBuilder } from "@core/controller/grpc-recorder/grpc-recorder.builder"
|
||||
import { testHooks } from "@core/controller/grpc-recorder/test-hooks"
|
||||
import { GrpcLogEntry } from "@core/controller/grpc-recorder/types"
|
||||
import * as sinon from "sinon"
|
||||
import { Controller } from ".."
|
||||
import { IRecorder } from "./grpc-recorder"
|
||||
import { GrpcRecorderBuilder } from "./grpc-recorder.builder"
|
||||
import { testHooks } from "./test-hooks"
|
||||
import { GrpcLogEntry } from "./types"
|
||||
|
||||
describe("test-hooks", () => {
|
||||
let cleanupSyntheticEntriesStub: sinon.SinonStub
|
||||
@@ -91,7 +91,15 @@ function testFilters(): GrpcRequestFilter[] {
|
||||
return [
|
||||
(req) => req.is_streaming,
|
||||
(req) => ["cline.UiService", "cline.McpService", "cline.WebService"].includes(req.service),
|
||||
(req) => ["refreshOpenRouterModels", "getAvailableTerminalProfiles"].includes(req.method),
|
||||
(req) =>
|
||||
[
|
||||
"refreshOpenRouterModels",
|
||||
"getAvailableTerminalProfiles",
|
||||
"showTaskWithId",
|
||||
"deleteTasksWithIds",
|
||||
"getTotalTasksSize",
|
||||
"cancelTask",
|
||||
].includes(req.method),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+144
-79
@@ -23,14 +23,21 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { PromptRegistry } from "../prompts/system-prompt"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
ensureCacheDirectoryExists,
|
||||
ensureMcpServersDirectoryExists,
|
||||
ensureSettingsDirectoryExists,
|
||||
GlobalFileNames,
|
||||
} from "../storage/disk"
|
||||
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
@@ -49,6 +56,7 @@ export class Controller {
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
ocaAuthService: OcaAuthService
|
||||
readonly stateManager: StateManager
|
||||
|
||||
// NEW: Add workspace manager (optional initially)
|
||||
@@ -61,9 +69,10 @@ export class Controller {
|
||||
this.id = id
|
||||
PromptRegistry.getInstance() // Ensure prompts and tools are registered
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.stateManager = new StateManager(context)
|
||||
this.authService = AuthService.getInstance(this)
|
||||
this.ocaAuthService = OcaAuthService.initialize(this)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
|
||||
// Initialize cache service asynchronously - critical for extension functionality
|
||||
this.stateManager
|
||||
@@ -86,7 +95,7 @@ export class Controller {
|
||||
this.stateManager.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("[Controller] Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await this.stateManager.reInitialize()
|
||||
await this.stateManager.reInitialize(this.task?.taskId)
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
@@ -108,7 +117,7 @@ export class Controller {
|
||||
this.mcpHub = new McpHub(
|
||||
() => ensureMcpServersDirectoryExists(),
|
||||
() => ensureSettingsDirectoryExists(this.context),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
ExtensionRegistryInfo.version,
|
||||
telemetryService,
|
||||
)
|
||||
|
||||
@@ -119,7 +128,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<Mode> {
|
||||
return this.stateManager.getGlobalStateKey("mode")
|
||||
return this.stateManager.getGlobalSettingsKey("mode")
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -163,6 +172,23 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Oca Auth methods
|
||||
async handleOcaSignOut() {
|
||||
try {
|
||||
await this.ocaAuthService.handleDeauth()
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of OCA",
|
||||
})
|
||||
} catch (_error) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "OCA Logout failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async setUserInfo(info?: UserInfo) {
|
||||
this.stateManager.setGlobalState("userInfo", info)
|
||||
}
|
||||
@@ -170,22 +196,15 @@ export class Controller {
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
|
||||
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.stateManager.getGlobalStateKey("mode")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
@@ -202,18 +221,6 @@ export class Controller {
|
||||
}
|
||||
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
|
||||
}
|
||||
// Apply remote feature flag gate to focus chain settings. Respect if user has disabled it.
|
||||
let focusChainEnabled: boolean
|
||||
if (focusChainSettings?.enabled === false) {
|
||||
focusChainEnabled = false
|
||||
} else {
|
||||
focusChainEnabled = Boolean(focusChainSettings?.enabled)
|
||||
}
|
||||
|
||||
const effectiveFocusChainSettings = {
|
||||
...(focusChainSettings || { enabled: true, remindClineInterval: 6 }),
|
||||
enabled: focusChainEnabled,
|
||||
}
|
||||
|
||||
// Initialize and persist the workspace manager (multi-root or single-root) with telemetry + fallback
|
||||
this.workspaceManager = await setupWorkspaceManager({
|
||||
@@ -230,15 +237,6 @@ export class Controller {
|
||||
() => this.postStateToWebview(),
|
||||
(taskId) => this.reinitExistingTaskFromId(taskId),
|
||||
() => this.cancelTask(),
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
effectiveFocusChainSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled ?? true,
|
||||
useAutoCondense ?? false,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
@@ -252,6 +250,11 @@ export class Controller {
|
||||
files,
|
||||
historyItem,
|
||||
)
|
||||
|
||||
// Load task settings after task creation
|
||||
if (this.task.taskId) {
|
||||
await this.stateManager.loadTaskSettings(this.task.taskId)
|
||||
}
|
||||
}
|
||||
|
||||
async reinitExistingTaskFromId(taskId: string) {
|
||||
@@ -268,6 +271,27 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async toggleActModeForYoloMode(): Promise<boolean> {
|
||||
const modeToSwitchTo: Mode = "act"
|
||||
|
||||
// Switch to act mode
|
||||
this.stateManager.setGlobalState("mode", modeToSwitchTo)
|
||||
|
||||
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
|
||||
if (this.task) {
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, ulid: this.task.ulid }, modeToSwitchTo)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
|
||||
// Additional safety
|
||||
if (this.task) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async togglePlanActMode(modeToSwitchTo: Mode, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = modeToSwitchTo === "act"
|
||||
|
||||
@@ -286,7 +310,6 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
|
||||
if (this.task) {
|
||||
this.task.updateMode(modeToSwitchTo)
|
||||
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default message
|
||||
@@ -344,7 +367,7 @@ export class Controller {
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
|
||||
// Get current settings to determine how to update providers
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
@@ -388,6 +411,57 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async handleOcaAuthCallback(code: string, state: string) {
|
||||
try {
|
||||
await this.ocaAuthService.handleAuthCallback(code, state)
|
||||
|
||||
const ocaProvider: ApiProvider = "oca"
|
||||
|
||||
// Get current settings to determine how to update providers
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
|
||||
const updatedConfig = { ...currentApiConfiguration }
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Only update the current mode's provider
|
||||
if (currentMode === "plan") {
|
||||
updatedConfig.planModeApiProvider = ocaProvider
|
||||
} else {
|
||||
updatedConfig.actModeApiProvider = ocaProvider
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
updatedConfig.planModeApiProvider = ocaProvider
|
||||
updatedConfig.actModeApiProvider = ocaProvider
|
||||
}
|
||||
|
||||
// Update the API configuration through cache service
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
this.stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to handle auth callback:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to OCA",
|
||||
})
|
||||
// Even on login failure, we preserve any existing tokens
|
||||
// Only clear tokens on explicit logout
|
||||
}
|
||||
}
|
||||
|
||||
// MCP Marketplace
|
||||
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
|
||||
try {
|
||||
@@ -522,15 +596,9 @@ export class Controller {
|
||||
// Dont send settingsButtonClicked because its bad ux if user is on welcome
|
||||
}
|
||||
|
||||
private async ensureCacheDirectoryExists(): Promise<string> {
|
||||
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
// Read OpenRouter models from disk cache
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
|
||||
@@ -541,10 +609,7 @@ export class Controller {
|
||||
|
||||
// Read Vercel AI Gateway models from disk cache
|
||||
async readVercelAiGatewayModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const vercelAiGatewayModelsFilePath = path.join(
|
||||
await this.ensureCacheDirectoryExists(),
|
||||
GlobalFileNames.vercelAiGatewayModels,
|
||||
)
|
||||
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
|
||||
const fileExists = await fileExistsAtPath(vercelAiGatewayModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(vercelAiGatewayModelsFilePath, "utf8")
|
||||
@@ -567,7 +632,7 @@ export class Controller {
|
||||
const history = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const historyItem = history.find((item) => item.id === id)
|
||||
if (historyItem) {
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
const taskDirPath = path.join(HostProvider.get().globalStorageFsPath, "tasks", id)
|
||||
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
|
||||
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
|
||||
const contextHistoryFilePath = path.join(taskDirPath, GlobalFileNames.contextHistory)
|
||||
@@ -619,32 +684,35 @@ export class Controller {
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const lastShownAnnouncementId = this.stateManager.getGlobalStateKey("lastShownAnnouncementId")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
|
||||
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.stateManager.getGlobalStateKey("mode")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
const preferredLanguage = this.stateManager.getGlobalSettingsKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalSettingsKey("openaiReasoningEffort")
|
||||
const mode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
|
||||
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
|
||||
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
|
||||
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
|
||||
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
|
||||
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
|
||||
const telemetrySetting = this.stateManager.getGlobalStateKey("telemetrySetting")
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const telemetrySetting = this.stateManager.getGlobalSettingsKey("telemetrySetting")
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const welcomeViewCompleted = Boolean(
|
||||
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
|
||||
)
|
||||
const customPrompt = this.stateManager.getGlobalStateKey("customPrompt")
|
||||
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt")
|
||||
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
@@ -659,21 +727,15 @@ export class Controller {
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 100) // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
|
||||
|
||||
const latestAnnouncementId = getLatestAnnouncementId(this.context)
|
||||
const latestAnnouncementId = getLatestAnnouncementId()
|
||||
const shouldShowAnnouncement = lastShownAnnouncementId !== latestAnnouncementId
|
||||
const platform = process.platform as Platform
|
||||
const distinctId = getDistinctId()
|
||||
const version = this.context.extension?.packageJSON?.version ?? ""
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
const extensionInfo = {
|
||||
name: this.context.extension?.packageJSON?.name,
|
||||
publisher: this.context.extension?.packageJSON?.publisher,
|
||||
}
|
||||
const version = ExtensionRegistryInfo.version
|
||||
|
||||
return {
|
||||
version,
|
||||
apiConfiguration,
|
||||
uriScheme,
|
||||
currentTaskItem,
|
||||
clineMessages,
|
||||
currentFocusChainChecklist: this.task?.taskState.currentFocusChainChecklist || null,
|
||||
@@ -685,6 +747,7 @@ export class Controller {
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
yoloModeToggled,
|
||||
useAutoCondense,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
@@ -710,7 +773,7 @@ export class Controller {
|
||||
taskHistory: processedTaskHistory,
|
||||
platform,
|
||||
shouldShowAnnouncement,
|
||||
extensionInfo,
|
||||
favoritedModelIds,
|
||||
// NEW: Add workspace information
|
||||
workspaceRoots: this.workspaceManager?.getRoots() ?? [],
|
||||
primaryRootIndex: this.workspaceManager?.getPrimaryIndex() ?? 0,
|
||||
@@ -720,6 +783,8 @@ export class Controller {
|
||||
|
||||
async clearTask() {
|
||||
if (this.task) {
|
||||
// Clear task settings cache when task ends
|
||||
await this.stateManager.clearTaskSettings(this.task.taskId)
|
||||
}
|
||||
await this.task?.abortTask()
|
||||
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { parsePrice } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -19,16 +20,16 @@ export async function refreshBasetenModels(
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
console.log("=== refreshBasetenModels called ===")
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.basetenModels)
|
||||
|
||||
// Get the Baseten API key from the controller's state
|
||||
const basetenApiKey = controller.stateManager.getSecretKey("basetenApiKey")
|
||||
|
||||
const models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
const models: Record<string, Partial<OpenRouterModelInfo> & { supportedFeatures?: string[] }> = {}
|
||||
try {
|
||||
if (!basetenApiKey) {
|
||||
console.log("No Baseten API key found, using static models as fallback")
|
||||
// Don't throw an error, just use static models
|
||||
// Don't throw an error, just use static models, althought this might be slightly out of date
|
||||
for (const [modelId, modelInfo] of Object.entries(basetenModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
@@ -69,25 +70,20 @@ export async function refreshBasetenModels(
|
||||
continue
|
||||
}
|
||||
|
||||
// Only include models that are listed in the static basetenModels
|
||||
if (!(rawModel.id in basetenModels)) {
|
||||
console.log(`Skipping model ${rawModel.id} - not in static basetenModels list`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have static pricing information for this model
|
||||
const staticModelInfo = basetenModels[rawModel.id as keyof typeof basetenModels]
|
||||
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: staticModelInfo?.maxTokens || 8192,
|
||||
contextWindow: staticModelInfo?.contextWindow || 8192,
|
||||
supportsImages: staticModelInfo?.supportsImages || false,
|
||||
const modelInfo: Partial<OpenRouterModelInfo> & { supportedFeatures?: string[] } = {
|
||||
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens,
|
||||
contextWindow: rawModel.context_length || staticModelInfo?.contextWindow,
|
||||
supportsImages: false, // Baseten model APIs does not support image input
|
||||
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
|
||||
inputPrice: staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: staticModelInfo?.outputPrice || 0,
|
||||
inputPrice: parsePrice(rawModel.pricing?.prompt) || staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: parsePrice(rawModel.pricing?.completion) || staticModelInfo?.outputPrice || 0,
|
||||
cacheWritesPrice: staticModelInfo?.cacheWritesPrice || 0,
|
||||
cacheReadsPrice: staticModelInfo?.cacheReadsPrice || 0,
|
||||
description: generateModelDescription(rawModel, staticModelInfo),
|
||||
supportedFeatures: rawModel.supported_features || [],
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
@@ -122,14 +118,12 @@ export async function refreshBasetenModels(
|
||||
console.error("Baseten API Error:", errorMessage)
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readBasetenModels(controller)
|
||||
const cachedModels = await readBasetenModels()
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
console.log("Using cached Baseten models")
|
||||
// Filter cached models to only include those in static basetenModels
|
||||
// Use all cached models (no filtering)
|
||||
for (const [modelId, modelInfo] of Object.entries(cachedModels)) {
|
||||
if (modelId in basetenModels) {
|
||||
models[modelId] = modelInfo
|
||||
}
|
||||
models[modelId] = modelInfo
|
||||
}
|
||||
} else {
|
||||
// Fall back to static models from shared/api.ts
|
||||
@@ -165,26 +159,18 @@ export async function refreshBasetenModels(
|
||||
cacheReadsPrice: model.cacheReadsPrice ?? 0,
|
||||
description: model.description ?? "",
|
||||
tiers: model.tiers ?? [],
|
||||
// Note: supportedFeatures is preserved as custom property but not part of OpenRouterModelInfo proto
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached Baseten models from disk
|
||||
*/
|
||||
async function readBasetenModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
|
||||
async function readBasetenModels(): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.basetenModels)
|
||||
const fileExists = await fileExistsAtPath(basetenModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
@@ -219,14 +205,47 @@ function isValidChatModel(rawModel: any): boolean {
|
||||
* Generates a descriptive name for the model
|
||||
*/
|
||||
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
|
||||
// Use static description if available
|
||||
// Use static description if available and preferred
|
||||
if (staticModelInfo?.description) {
|
||||
return staticModelInfo.description
|
||||
}
|
||||
|
||||
// Generate description based on model characteristics
|
||||
const modelId = rawModel.id
|
||||
const ownedBy = rawModel.owned_by || "Unknown"
|
||||
// Use API description if available
|
||||
if (rawModel.description) {
|
||||
const contextWindow = rawModel.context_length
|
||||
const quantization = rawModel.quantization
|
||||
const features = rawModel.supported_features || []
|
||||
|
||||
return `${ownedBy} model: ${modelId}`
|
||||
let description = rawModel.description
|
||||
|
||||
// Add technical details if available
|
||||
const technicalDetails = []
|
||||
if (contextWindow) {
|
||||
technicalDetails.push(`${contextWindow.toLocaleString()} token context`)
|
||||
}
|
||||
if (quantization) {
|
||||
technicalDetails.push(`${quantization} precision`)
|
||||
}
|
||||
if (features.length > 0) {
|
||||
const featureList = features.join(", ")
|
||||
technicalDetails.push(`supports ${featureList}`)
|
||||
}
|
||||
|
||||
if (technicalDetails.length > 0) {
|
||||
description += ` (${technicalDetails.join(", ")})`
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
// Fallback: use name or model ID
|
||||
const modelName = rawModel.name || rawModel.id
|
||||
const contextWindow = rawModel.context_length
|
||||
const ownedBy = rawModel.owned_by || "Baseten"
|
||||
|
||||
if (contextWindow) {
|
||||
return `${ownedBy} ${modelName} with ${contextWindow.toLocaleString()} token context window`
|
||||
}
|
||||
|
||||
return `${ownedBy} model: ${modelName}`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
@@ -16,7 +16,7 @@ import { Controller } from ".."
|
||||
* @returns Response containing the Groq models
|
||||
*/
|
||||
export async function refreshGroqModels(controller: Controller, _request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.groqModels)
|
||||
|
||||
const groqApiKey = controller.stateManager.getSecretKey("groqApiKey")
|
||||
|
||||
@@ -165,7 +165,7 @@ export async function refreshGroqModels(controller: Controller, _request: EmptyR
|
||||
* Reads cached Groq models from disk
|
||||
*/
|
||||
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.groqModels)
|
||||
const fileExists = await fileExistsAtPath(groqModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
@@ -246,12 +246,3 @@ function generateModelDescription(rawModel: any, staticModelInfo?: any): string
|
||||
|
||||
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
@@ -5,21 +5,9 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ensureCacheDirectoryExists } from "@/core/storage/disk"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
try {
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
} catch (_error) {
|
||||
// Directory might already exist
|
||||
}
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the Hugging Face models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
@@ -27,10 +15,10 @@ async function ensureCacheDirectoryExists(controller: Controller): Promise<strin
|
||||
* @returns Response containing the Hugging Face models
|
||||
*/
|
||||
export async function refreshHuggingFaceModels(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
|
||||
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(), "huggingface_models.json")
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
|
||||
import axios from "axios"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Refreshes the Oca models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Oca models
|
||||
*/
|
||||
export async function refreshOcaModels(controller: Controller, request: StringRequest): Promise<OcaCompatibleModelInfo> {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
const models: Record<string, OcaModelInfo> = {}
|
||||
let defaultModelId: string | undefined
|
||||
const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken()
|
||||
const baseUrl = request.value || DEFAULT_OCA_BASE_URL
|
||||
const modelsUrl = `${baseUrl}/v1/model/info`
|
||||
const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh")
|
||||
try {
|
||||
Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`)
|
||||
const response = await axios.get(modelsUrl, { headers, ...getProxyAgents() })
|
||||
if (response.data?.data) {
|
||||
if (response.data.data.length === 0) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No models found. Did you set up your OCA access (possibly through entitlements)?",
|
||||
})
|
||||
}
|
||||
for (const model of response.data.data) {
|
||||
const modelId = model.litellm_params?.model
|
||||
if (typeof modelId !== "string" || !modelId) {
|
||||
continue
|
||||
}
|
||||
if (!defaultModelId) {
|
||||
defaultModelId = modelId
|
||||
}
|
||||
const modelInfo = model.model_info
|
||||
models[modelId] = OcaModelInfo.create({
|
||||
maxTokens: model.litellm_params?.max_tokens || -1,
|
||||
contextWindow: modelInfo.context_window,
|
||||
supportsImages: modelInfo.supports_vision || false,
|
||||
supportsPromptCache: modelInfo.supports_caching || false,
|
||||
inputPrice: parsePrice(modelInfo.input_price) || 0,
|
||||
outputPrice: parsePrice(modelInfo.output_price) || 0,
|
||||
cacheWritesPrice: parsePrice(modelInfo.caching_price) || 0,
|
||||
cacheReadsPrice: parsePrice(modelInfo.cached_price) || 0,
|
||||
description: modelInfo.description,
|
||||
thinkingConfig: modelInfo.thinking_config,
|
||||
surveyContent: modelInfo.survey_content,
|
||||
surveyId: modelInfo.survey_id,
|
||||
temperature: modelInfo.temperature || 0,
|
||||
banner: modelInfo.banner,
|
||||
modelName: modelId,
|
||||
})
|
||||
}
|
||||
console.log("OCA models fetched", models)
|
||||
|
||||
// Fetch current config
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Which mode(s) to update?
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = (await controller.getCurrentMode?.()) ?? "plan"
|
||||
const planModeSelectedModelId =
|
||||
apiConfiguration?.planModeOcaModelId && models[apiConfiguration.planModeOcaModelId]
|
||||
? apiConfiguration.planModeOcaModelId
|
||||
: defaultModelId!
|
||||
const actModeSelectedModelId =
|
||||
apiConfiguration?.actModeOcaModelId && models[apiConfiguration.actModeOcaModelId]
|
||||
? apiConfiguration.actModeOcaModelId
|
||||
: defaultModelId!
|
||||
|
||||
// Save new model selection(s) to configuration object, per plan/act mode setting
|
||||
if (planActSeparateModelsSetting) {
|
||||
if (currentMode === "plan") {
|
||||
updatedConfig.planModeOcaModelId = planModeSelectedModelId
|
||||
updatedConfig.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
} else {
|
||||
updatedConfig.actModeOcaModelId = actModeSelectedModelId
|
||||
updatedConfig.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
}
|
||||
} else {
|
||||
updatedConfig.planModeOcaModelId = planModeSelectedModelId
|
||||
updatedConfig.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
updatedConfig.actModeOcaModelId = actModeSelectedModelId
|
||||
updatedConfig.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
}
|
||||
|
||||
controller.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Refreshed OCA models from ${baseUrl}`,
|
||||
})
|
||||
await controller.postStateToWebview?.()
|
||||
} else {
|
||||
console.error("Invalid response from OCA API")
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to fetch OCA models. Please check your configuration from ${baseUrl}`,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
let userMsg
|
||||
if (err.response) {
|
||||
// The request was made and the server responded with a status code that falls out of the range of 2xx
|
||||
userMsg = `Did you set up your OCA access (possibly through entitlements)? OCA service returned ${err.response.status} ${err.response.statusText}.`
|
||||
} else if (err.request) {
|
||||
// The request was made but no response was received
|
||||
userMsg = `Unable to access the OCA backend. Is your endpoint and proxy configured properly? Please see the troubleshooting guide.`
|
||||
} else {
|
||||
userMsg = err.message
|
||||
console.error(userMsg, err)
|
||||
}
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error refreshing OCA models. ` + userMsg + ` opc-request-id: ${headers["opc-request-id"]}`,
|
||||
})
|
||||
return OcaCompatibleModelInfo.create({ error: userMsg })
|
||||
}
|
||||
return OcaCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
@@ -6,7 +6,7 @@ import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { CLAUDE_SONNET_4_1M_TIERS, clineMicrowaveAlphaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
|
||||
import { CLAUDE_SONNET_4_1M_TIERS, clineCodeSupernovaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
|
||||
import { Controller } from ".."
|
||||
|
||||
type OpenRouterSupportedParams =
|
||||
@@ -77,7 +77,7 @@ export async function refreshOpenRouterModels(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
@@ -222,20 +222,20 @@ export async function refreshOpenRouterModels(
|
||||
}
|
||||
}
|
||||
|
||||
// Add hardcoded cline/sonic model
|
||||
models["cline/sonic"] = OpenRouterModelInfo.create({
|
||||
maxTokens: clineMicrowaveAlphaModelInfo.maxTokens ?? 0,
|
||||
contextWindow: clineMicrowaveAlphaModelInfo.contextWindow ?? 0,
|
||||
supportsImages: clineMicrowaveAlphaModelInfo.supportsImages ?? false,
|
||||
supportsPromptCache: clineMicrowaveAlphaModelInfo.supportsPromptCache ?? false,
|
||||
inputPrice: clineMicrowaveAlphaModelInfo.inputPrice ?? 0,
|
||||
outputPrice: clineMicrowaveAlphaModelInfo.outputPrice ?? 0,
|
||||
cacheWritesPrice: clineMicrowaveAlphaModelInfo.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: clineMicrowaveAlphaModelInfo.cacheReadsPrice ?? 0,
|
||||
description: clineMicrowaveAlphaModelInfo.description ?? "",
|
||||
thinkingConfig: clineMicrowaveAlphaModelInfo.thinkingConfig ?? undefined,
|
||||
supportsGlobalEndpoint: clineMicrowaveAlphaModelInfo.supportsGlobalEndpoint ?? undefined,
|
||||
tiers: clineMicrowaveAlphaModelInfo.tiers ?? [],
|
||||
// Add hardcoded stealth model
|
||||
models["cline/code-supernova"] = OpenRouterModelInfo.create({
|
||||
maxTokens: clineCodeSupernovaModelInfo.maxTokens ?? 0,
|
||||
contextWindow: clineCodeSupernovaModelInfo.contextWindow ?? 0,
|
||||
supportsImages: clineCodeSupernovaModelInfo.supportsImages ?? false,
|
||||
supportsPromptCache: clineCodeSupernovaModelInfo.supportsPromptCache ?? false,
|
||||
inputPrice: clineCodeSupernovaModelInfo.inputPrice ?? 0,
|
||||
outputPrice: clineCodeSupernovaModelInfo.outputPrice ?? 0,
|
||||
cacheWritesPrice: clineCodeSupernovaModelInfo.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: clineCodeSupernovaModelInfo.cacheReadsPrice ?? 0,
|
||||
description: clineCodeSupernovaModelInfo.description ?? "",
|
||||
thinkingConfig: clineCodeSupernovaModelInfo.thinkingConfig ?? undefined,
|
||||
supportsGlobalEndpoint: clineCodeSupernovaModelInfo.supportsGlobalEndpoint ?? undefined,
|
||||
tiers: clineCodeSupernovaModelInfo.tiers ?? [],
|
||||
})
|
||||
} else {
|
||||
console.error("Invalid response from OpenRouter API")
|
||||
@@ -259,7 +259,7 @@ export async function refreshOpenRouterModels(
|
||||
* Reads cached OpenRouter models from disk
|
||||
*/
|
||||
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
@@ -272,12 +272,3 @@ async function readOpenRouterModels(controller: Controller): Promise<Record<stri
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
|
||||
const models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
const apiKey = controller.stateManager.getSecretKey("requestyApiKey")
|
||||
const baseUrl = controller.stateManager.getGlobalStateKey("requestyBaseUrl")
|
||||
const baseUrl = controller.stateManager.getGlobalSettingsKey("requestyBaseUrl")
|
||||
|
||||
const resolvedUrl = toRequestyServiceUrl(baseUrl)
|
||||
const url = new URL(`${resolvedUrl.pathname}/models`, resolvedUrl).toString()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
@@ -14,13 +14,10 @@ import { Controller } from ".."
|
||||
* @returns Response containing Vercel AI Gateway models
|
||||
*/
|
||||
export async function refreshVercelAiGatewayModels(
|
||||
controller: Controller,
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const vercelAiGatewayModelsFilePath = path.join(
|
||||
await ensureCacheDirectoryExists(controller),
|
||||
GlobalFileNames.vercelAiGatewayModels,
|
||||
)
|
||||
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
|
||||
@@ -65,7 +62,7 @@ export async function refreshVercelAiGatewayModels(
|
||||
console.error("Error fetching Vercel AI Gateway models:", error)
|
||||
|
||||
// If we failed to fetch models, try to read cached models
|
||||
const cachedModels = await readVercelAiGatewayModels(controller)
|
||||
const cachedModels = await readVercelAiGatewayModels()
|
||||
if (cachedModels) {
|
||||
models = cachedModels
|
||||
}
|
||||
@@ -77,11 +74,8 @@ export async function refreshVercelAiGatewayModels(
|
||||
/**
|
||||
* Reads cached Vercel AI Gateway models from disk
|
||||
*/
|
||||
async function readVercelAiGatewayModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
|
||||
const vercelAiGatewayModelsFilePath = path.join(
|
||||
await ensureCacheDirectoryExists(controller),
|
||||
GlobalFileNames.vercelAiGatewayModels,
|
||||
)
|
||||
async function readVercelAiGatewayModels(): Promise<Record<string, OpenRouterModelInfo> | undefined> {
|
||||
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
|
||||
const fileExists = await fileExistsAtPath(vercelAiGatewayModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
@@ -94,12 +88,3 @@ async function readVercelAiGatewayModels(controller: Controller): Promise<Record
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EmptyRequest, String as ProtoString } from "@shared/proto/cline/common"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
* Generates a secure nonce for state validation, stores it in secrets,
|
||||
* and opens the authentication URL in the external browser.
|
||||
*
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function ocaAccountLoginClicked(_controller: Controller, _: EmptyRequest): Promise<ProtoString> {
|
||||
return await OcaAuthService.getInstance().createAuthRequest()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Handles the account logout action
|
||||
* @param controller The controller instance
|
||||
* @param _request The empty request object
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function ocaAccountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
await controller.handleOcaSignOut()
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OcaAuthState } from "@shared/proto/cline/oca_account"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { Controller } from ".."
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
|
||||
export async function ocaSubscribeToAuthStatusUpdate(
|
||||
_controller: Controller,
|
||||
request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<OcaAuthState>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
return OcaAuthService.getInstance().subscribeToAuthStatusUpdate(request, responseStream, requestId)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -15,21 +15,15 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
const favoritedModelIds = controller.stateManager.getGlobalStateKey("favoritedModelIds")
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(modelId)
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
// Update the complete API configuration through cache service
|
||||
const updatedApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
favoritedModelIds: updatedFavorites,
|
||||
}
|
||||
controller.stateManager.setApiConfiguration(updatedApiConfiguration)
|
||||
controller.stateManager.setGlobalState("favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
|
||||
@@ -18,12 +18,18 @@ export async function updateAutoApprovalSettings(controller: Controller, request
|
||||
if (incomingVersion > currentVersion) {
|
||||
const settings = convertProtoToAutoApprovalSettings(request)
|
||||
|
||||
controller.stateManager.setGlobalState("autoApprovalSettings", settings)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.updateAutoApprovalSettings(settings)
|
||||
const maxRequestsChanged =
|
||||
controller.stateManager.getGlobalSettingsKey("autoApprovalSettings").maxRequests !== settings.maxRequests
|
||||
|
||||
// Reset counter if max requests limit changed
|
||||
if (maxRequestsChanged) {
|
||||
controller.task.resetConsecutiveAutoApprovedRequestsCount()
|
||||
}
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("autoApprovalSettings", settings)
|
||||
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
OpenaiReasoningEffort as ProtoOpenaiReasoningEffort,
|
||||
UpdateSettingsRequest,
|
||||
} from "@shared/proto/cline/state"
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
@@ -25,14 +25,29 @@ import { Controller } from ".."
|
||||
*/
|
||||
export async function updateSettings(controller: Controller, request: UpdateSettingsRequest): Promise<Empty> {
|
||||
try {
|
||||
// Update API configuration
|
||||
if (request.apiConfiguration) {
|
||||
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
|
||||
controller.stateManager.setApiConfiguration(apiConfiguration)
|
||||
const protoApiConfiguration = request.apiConfiguration
|
||||
|
||||
const convertedApiConfigurationFromProto = {
|
||||
...protoApiConfiguration,
|
||||
// Convert proto ApiProvider enums to native string types
|
||||
planModeApiProvider: protoApiConfiguration.planModeApiProvider
|
||||
? convertProtoToApiProvider(protoApiConfiguration.planModeApiProvider)
|
||||
: undefined,
|
||||
actModeApiProvider: protoApiConfiguration.actModeApiProvider
|
||||
? convertProtoToApiProvider(protoApiConfiguration.actModeApiProvider)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
controller.stateManager.setApiConfiguration(convertedApiConfigurationFromProto)
|
||||
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
controller.task.api = buildApiHandler({ ...apiConfiguration, ulid: controller.task.ulid }, currentMode)
|
||||
const apiConfigForHandler = {
|
||||
...convertedApiConfigurationFromProto,
|
||||
ulid: controller.task.ulid,
|
||||
}
|
||||
controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +98,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
|
||||
if (request.mode !== undefined) {
|
||||
const mode = request.mode === PlanActMode.PLAN ? "plan" : "act"
|
||||
if (controller.task) {
|
||||
controller.task.updateMode(mode)
|
||||
}
|
||||
controller.stateManager.setGlobalState("mode", mode)
|
||||
}
|
||||
|
||||
@@ -109,17 +121,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
throw new Error(`Invalid OpenAI reasoning effort value: ${request.openaiReasoningEffort}`)
|
||||
}
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.openaiReasoningEffort = reasoningEffort
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("openaiReasoningEffort", reasoningEffort)
|
||||
}
|
||||
|
||||
if (request.preferredLanguage !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.preferredLanguage = request.preferredLanguage
|
||||
}
|
||||
controller.stateManager.setGlobalState("preferredLanguage", request.preferredLanguage)
|
||||
}
|
||||
|
||||
@@ -140,16 +145,25 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
|
||||
// Update strict plan mode setting
|
||||
if (request.strictPlanModeEnabled !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.updateStrictPlanMode(request.strictPlanModeEnabled)
|
||||
}
|
||||
controller.stateManager.setGlobalState("strictPlanModeEnabled", request.strictPlanModeEnabled)
|
||||
}
|
||||
|
||||
// Update yolo mode setting
|
||||
if (request.yoloModeToggled !== undefined) {
|
||||
if (controller.task) {
|
||||
telemetryService.captureYoloModeToggle(controller.task.ulid, request.yoloModeToggled)
|
||||
}
|
||||
controller.stateManager.setGlobalState("yoloModeToggled", request.yoloModeToggled)
|
||||
}
|
||||
|
||||
// Update auto-condense setting
|
||||
if (request.useAutoCondense !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.updateUseAutoCondense(request.useAutoCondense)
|
||||
telemetryService.captureAutoCondenseToggle(
|
||||
controller.task.ulid,
|
||||
request.useAutoCondense,
|
||||
controller.task.api.getModel().id,
|
||||
)
|
||||
}
|
||||
controller.stateManager.setGlobalState("useAutoCondense", request.useAutoCondense)
|
||||
}
|
||||
@@ -157,7 +171,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
// Update focus chain settings
|
||||
if (request.focusChainSettings !== undefined) {
|
||||
{
|
||||
const currentSettings = controller.stateManager.getGlobalStateKey("focusChainSettings")
|
||||
const currentSettings = controller.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
const wasEnabled = currentSettings?.enabled ?? false
|
||||
const isEnabled = request.focusChainSettings.enabled
|
||||
|
||||
@@ -183,7 +197,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
// Update browser settings
|
||||
if (request.browserSettings !== undefined) {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const currentSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
@@ -218,12 +232,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
|
||||
// Update global state with new settings
|
||||
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
}
|
||||
|
||||
// Update default terminal profile
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
|
||||
// Delete non-favorited task directories
|
||||
const preserveTaskIds = favoritedTasks.map((task) => task.id)
|
||||
await cleanupTaskFiles(controller, preserveTaskIds)
|
||||
await cleanupTaskFiles(preserveTaskIds)
|
||||
|
||||
// Update webview
|
||||
try {
|
||||
@@ -91,13 +91,13 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
|
||||
try {
|
||||
// Remove all contents of tasks directory
|
||||
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
|
||||
const taskDirPath = path.join(HostProvider.get().globalStorageFsPath, "tasks")
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
await fs.rm(taskDirPath, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
// Remove checkpoints directory contents
|
||||
const checkpointsDirPath = path.join(controller.context.globalStorageUri.fsPath, "checkpoints")
|
||||
const checkpointsDirPath = path.join(HostProvider.get().globalStorageFsPath, "checkpoints")
|
||||
if (await fileExistsAtPath(checkpointsDirPath)) {
|
||||
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
|
||||
}
|
||||
@@ -127,8 +127,8 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
/**
|
||||
* Helper function to cleanup task files while preserving specified tasks
|
||||
*/
|
||||
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
|
||||
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
|
||||
async function cleanupTaskFiles(preserveTaskIds: string[]) {
|
||||
const taskDirPath = path.join(HostProvider.get().globalStorageFsPath, "tasks")
|
||||
|
||||
try {
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
|
||||
@@ -80,8 +80,8 @@ async function deleteTaskWithId(controller: Controller, id: string): Promise<voi
|
||||
|
||||
// If no tasks remain, clean up everything
|
||||
if (updatedTaskHistory.length === 0) {
|
||||
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
|
||||
const checkpointsDirPath = path.join(controller.context.globalStorageUri.fsPath, "checkpoints")
|
||||
const taskDirPath = path.join(HostProvider.get().globalStorageFsPath, "tasks")
|
||||
const checkpointsDirPath = path.join(HostProvider.get().globalStorageFsPath, "checkpoints")
|
||||
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
await fs.rm(taskDirPath, { recursive: true, force: true })
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Controller } from ".."
|
||||
* @param _request The empty request
|
||||
* @returns The total size as an Int64 value
|
||||
*/
|
||||
export async function getTotalTasksSize(controller: Controller, _request: EmptyRequest): Promise<Int64> {
|
||||
const totalSize = await calculateTotalTasksSize(controller.context.globalStorageUri.fsPath)
|
||||
return Int64.create({ value: totalSize || 0 })
|
||||
export async function getTotalTasksSize(_controller: Controller, _request: EmptyRequest): Promise<Int64> {
|
||||
const totalSize = await calculateTotalTasksSize()
|
||||
return { value: totalSize || 0 }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
if (response && response.models) {
|
||||
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -76,7 +76,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Groq (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -122,7 +122,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Baseten (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Vercel AI Gateway (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Controller } from "../index"
|
||||
*/
|
||||
export async function onDidShowAnnouncement(controller: Controller, _request: EmptyRequest): Promise<Boolean> {
|
||||
try {
|
||||
const latestAnnouncementId = getLatestAnnouncementId(controller.context)
|
||||
const latestAnnouncementId = getLatestAnnouncementId()
|
||||
// Update the lastShownAnnouncementId to the current latestAnnouncementId
|
||||
controller.stateManager.setGlobalState("lastShownAnnouncementId", latestAnnouncementId)
|
||||
return Boolean.create({ value: false })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -31,6 +31,7 @@ describe("PromptBuilder", () => {
|
||||
},
|
||||
isTesting: true,
|
||||
providerInfo: mockProviderInfo,
|
||||
yoloModeToggled: false,
|
||||
}
|
||||
|
||||
const mockComponents: ComponentRegistry = {
|
||||
@@ -126,7 +127,12 @@ describe("PromptBuilder", () => {
|
||||
const customComponents: ComponentRegistry = {
|
||||
...mockComponents,
|
||||
SYSTEM_INFO_SECTION: async (variant) => {
|
||||
const template = variant.componentOverrides?.SYSTEM_INFO_SECTION?.template || "DEFAULT"
|
||||
let template = variant.componentOverrides?.SYSTEM_INFO_SECTION?.template || "DEFAULT"
|
||||
|
||||
if (typeof template === "function") {
|
||||
const mockContext = { cwd: "/test", yoloModeToggled: false } as SystemPromptContext
|
||||
template = template(mockContext)
|
||||
}
|
||||
return template.replace("{{os}}", "Linux").replace("{{shell}}", "bash")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { expect } from "chai"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
import { mockProviderInfo } from "./integration.test"
|
||||
|
||||
describe("TemplateEngine", () => {
|
||||
let templateEngine: TemplateEngine
|
||||
@@ -9,10 +12,36 @@ describe("TemplateEngine", () => {
|
||||
})
|
||||
|
||||
describe("resolve", () => {
|
||||
const mockContext: SystemPromptContext = {
|
||||
cwd: "/test/project",
|
||||
ide: "TestIde",
|
||||
supportsBrowserUse: true,
|
||||
mcpHub: {
|
||||
getServers: () => [],
|
||||
getMcpServersPath: () => "/test/mcp-servers",
|
||||
getSettingsDirectoryPath: () => "/test/settings",
|
||||
clientVersion: "1.0.0",
|
||||
disposables: [],
|
||||
} as unknown as McpHub,
|
||||
focusChainSettings: {
|
||||
enabled: true,
|
||||
remindClineInterval: 6,
|
||||
},
|
||||
browserSettings: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
},
|
||||
isTesting: true,
|
||||
providerInfo: mockProviderInfo,
|
||||
yoloModeToggled: false,
|
||||
}
|
||||
|
||||
it("should resolve simple placeholders", () => {
|
||||
const template = "Hello {{name}}!"
|
||||
const placeholders = { name: "World" }
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal("Hello World!")
|
||||
})
|
||||
|
||||
@@ -23,7 +52,7 @@ describe("TemplateEngine", () => {
|
||||
name: "Alice",
|
||||
day: "Monday",
|
||||
}
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal("Hello Alice, today is Monday")
|
||||
})
|
||||
|
||||
@@ -35,14 +64,14 @@ describe("TemplateEngine", () => {
|
||||
age: 30,
|
||||
},
|
||||
}
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal("User: John, Age: 30")
|
||||
})
|
||||
|
||||
it("should preserve unmatched placeholders", () => {
|
||||
const template = "Hello {{name}}, your {{missing}} is pending"
|
||||
const placeholders = { name: "Alice" }
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal("Hello Alice, your {{missing}} is pending")
|
||||
})
|
||||
|
||||
@@ -51,14 +80,14 @@ describe("TemplateEngine", () => {
|
||||
const placeholders = {
|
||||
config: { key: "value", items: [1, 2, 3] },
|
||||
}
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal('Config: {"key":"value","items":[1,2,3]}')
|
||||
})
|
||||
|
||||
it("should handle whitespace around placeholder names", () => {
|
||||
const template = "Hello {{ name }}, welcome to {{ place }}"
|
||||
const placeholders = { name: "Bob", place: "Paradise" }
|
||||
const result = templateEngine.resolve(template, placeholders)
|
||||
const result = templateEngine.resolve(template, mockContext, placeholders)
|
||||
expect(result).to.equal("Hello Bob, welcome to Paradise")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
const ACT_VS_PLAN_MODE_TEMPLATE_TEXT = `ACT MODE V.S. PLAN MODE
|
||||
const getActVsPlanModeTemplateText = (context: SystemPromptContext) => `ACT MODE V.S. PLAN MODE
|
||||
|
||||
In each user message, the environment_details will specify the current mode. There are two modes:
|
||||
|
||||
@@ -15,13 +15,13 @@ In each user message, the environment_details will specify the current mode. The
|
||||
## What is PLAN MODE?
|
||||
|
||||
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task.${context.yoloModeToggled !== true ? " You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task." : ""}
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Present the plan to the user using the plan_mode_respond tool.
|
||||
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
|
||||
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.`
|
||||
|
||||
export async function getActVsPlanModeSection(variant: PromptVariant, _context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.ACT_VS_PLAN]?.template || ACT_VS_PLAN_MODE_TEMPLATE_TEXT
|
||||
export async function getActVsPlanModeSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.ACT_VS_PLAN]?.template || getActVsPlanModeTemplateText
|
||||
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ const AGENT_ROLE = [
|
||||
"with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
|
||||
]
|
||||
|
||||
export async function getAgentRoleSection(variant: PromptVariant, _context: SystemPromptContext): Promise<string> {
|
||||
export async function getAgentRoleSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.AGENT_ROLE]?.template || AGENT_ROLE.join(" ")
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getTodoListSection(variant: PromptVariant, context: System
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.TODO]?.template || TODO_LIST_TEMPLATE_TEXT
|
||||
|
||||
const templateEngine = new TemplateEngine()
|
||||
return templateEngine.resolve(template, {
|
||||
return templateEngine.resolve(template, context, {
|
||||
// Add any todo-specific placeholders here
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
const CAPABILITIES_TEMPLATE_TEXT = `CAPABILITIES
|
||||
const getCapabilitiesTemplateText = (context: SystemPromptContext) => `CAPABILITIES
|
||||
|
||||
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search{{BROWSER_SUPPORT}}, read and edit files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
|
||||
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search{{BROWSER_SUPPORT}}, read and edit files${context.yoloModeToggled !== true ? ", and ask follow-up questions" : ""}. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
|
||||
- When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('{{CWD}}') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
@@ -13,7 +13,7 @@ const CAPABILITIES_TEMPLATE_TEXT = `CAPABILITIES
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.`
|
||||
|
||||
export async function getCapabilitiesSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.CAPABILITIES]?.template || CAPABILITIES_TEMPLATE_TEXT
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.CAPABILITIES]?.template || getCapabilitiesTemplateText
|
||||
|
||||
const browserSupport = context.supportsBrowserUse ? ", use the browser" : ""
|
||||
const browserCapabilities = context.supportsBrowserUse
|
||||
@@ -21,7 +21,7 @@ export async function getCapabilitiesSection(variant: PromptVariant, context: Sy
|
||||
: ""
|
||||
|
||||
const templateEngine = new TemplateEngine()
|
||||
return templateEngine.resolve(template, {
|
||||
return templateEngine.resolve(template, context, {
|
||||
BROWSER_SUPPORT: browserSupport,
|
||||
BROWSER_CAPABILITIES: browserCapabilities,
|
||||
CWD: context.cwd || process.cwd(),
|
||||
|
||||
@@ -74,8 +74,8 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.`
|
||||
|
||||
export async function getEditingFilesSection(variant: PromptVariant, _context: SystemPromptContext): Promise<string> {
|
||||
export async function getEditingFilesSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.EDITING_FILES]?.template || EDITING_FILES_TEMPLATE_TEXT
|
||||
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ export async function getFeedbackSection(variant: PromptVariant, context: System
|
||||
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.FEEDBACK]?.template || FEEDBACK_TEMPLATE_TEXT
|
||||
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -19,13 +19,14 @@ export async function getMcp(variant: PromptVariant, context: SystemPromptContex
|
||||
if (servers.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return await getMcpServers(servers, variant)
|
||||
return await getMcpServers(servers, variant, context)
|
||||
}
|
||||
|
||||
async function getMcpServers(servers: McpServer[], variant: PromptVariant): Promise<string> {
|
||||
async function getMcpServers(servers: McpServer[], variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.MCP]?.template || MCP_TEMPLATE_TEXT
|
||||
|
||||
const serversList = servers.length > 0 ? formatMcpServersList(servers) : "(No MCP servers currently connected)"
|
||||
return new TemplateEngine().resolve(template, {
|
||||
return new TemplateEngine().resolve(template, context, {
|
||||
MCP_SERVERS_LIST: serversList,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
const OBJECTIVE_TEMPLATE_TEXT = `OBJECTIVE
|
||||
const getObjectiveTemplateText = (context: SystemPromptContext) => `OBJECTIVE
|
||||
|
||||
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
|
||||
|
||||
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
|
||||
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
|
||||
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
|
||||
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params)${context.yoloModeToggled !== true ? " and instead, ask the user to provide the missing parameters using the ask_followup_question tool" : ""}. DO NOT ask for more information on optional parameters if it is not provided.
|
||||
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
|
||||
export async function getObjectiveSection(variant: PromptVariant, _context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.OBJECTIVE]?.template || OBJECTIVE_TEMPLATE_TEXT
|
||||
export async function getObjectiveSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.OBJECTIVE]?.template || getObjectiveTemplateText
|
||||
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const BROWSER_RULES = `- The user may ask generic non-development tasks, such as
|
||||
|
||||
const BROWSER_WAIT_RULES = ` Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser.`
|
||||
|
||||
const RULES_TEMPLATE_TEXT = `RULES
|
||||
const getRulesTemplateText = (context: SystemPromptContext) => `RULES
|
||||
|
||||
- Your current working directory is: {{CWD}}
|
||||
- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '{{CWD}}', so be sure to pass in the correct 'path' parameter when using tools that require a path.
|
||||
@@ -18,8 +18,8 @@ const RULES_TEMPLATE_TEXT = `RULES
|
||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
|
||||
- ${context.yoloModeToggled !== true ? "You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so" : "Use your available tools and apply your best judgment to accomplish the task without asking the user any followup questions, making reasonable assumptions from the provided context"}. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly.${context.yoloModeToggled !== true ? " If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you." : ""}
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
|
||||
{{BROWSER_RULES}}- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
@@ -34,12 +34,12 @@ const RULES_TEMPLATE_TEXT = `RULES
|
||||
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.`
|
||||
|
||||
export async function getRulesSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.RULES]?.template || RULES_TEMPLATE_TEXT
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.RULES]?.template || getRulesTemplateText
|
||||
|
||||
const browserRules = context.supportsBrowserUse ? BROWSER_RULES : ""
|
||||
const browserWaitRules = context.supportsBrowserUse ? BROWSER_WAIT_RULES : ""
|
||||
|
||||
return new TemplateEngine().resolve(template, {
|
||||
return new TemplateEngine().resolve(template, context, {
|
||||
CWD: context.cwd || process.cwd(),
|
||||
BROWSER_RULES: browserRules,
|
||||
BROWSER_WAIT_RULES: browserWaitRules,
|
||||
|
||||
@@ -62,9 +62,10 @@ export async function getSystemInfo(variant: PromptVariant, context: SystemPromp
|
||||
WORKSPACE_TITLE = "Current Working Directory"
|
||||
workingDirInfo = info.workingDir
|
||||
}
|
||||
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.SYSTEM_INFO]?.template || SYSTEM_INFO_TEMPLATE_TEXT
|
||||
|
||||
return new TemplateEngine().resolve(template, {
|
||||
return new TemplateEngine().resolve(template, context, {
|
||||
os: info.os,
|
||||
ide: info.ide,
|
||||
shell: info.shell,
|
||||
|
||||
@@ -31,5 +31,5 @@ export async function getUpdatingTaskProgress(variant: PromptVariant, context: S
|
||||
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.TASK_PROGRESS]?.template || UPDATING_TASK_PROGRESS
|
||||
|
||||
return new TemplateEngine().resolve(template, {})
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export async function getToolUseExamplesSection(_variant: PromptVariant, context
|
||||
// Return the placeholder that will be replaced with actual tools
|
||||
const focusChainEnabled = context.focusChainSettings?.enabled
|
||||
|
||||
return new TemplateEngine().resolve(TOOL_USE_EXAMPLES_TEMPLATE_TEXT, {
|
||||
return new TemplateEngine().resolve(TOOL_USE_EXAMPLES_TEMPLATE_TEXT, context, {
|
||||
FOCUS_CHAIN_EXAMPLE_BASH: focusChainEnabled ? FOCUS_CHAIN_EXAMPLE_BASH : "",
|
||||
FOCUS_CHAIN_EXAMPLE_NEW_FILE: focusChainEnabled ? FOCUS_CHAIN_EXAMPLE_NEW_FILE : "",
|
||||
FOCUS_CHAIN_EXAMPLE_EDIT: focusChainEnabled ? FOCUS_CHAIN_EXAMPLE_EDIT : "",
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function getToolUseFormattingSection(_variant: PromptVariant, conte
|
||||
const focusChainEnabled = context.focusChainSettings?.enabled
|
||||
|
||||
const templateEngine = new TemplateEngine()
|
||||
return templateEngine.resolve(template, {
|
||||
return templateEngine.resolve(template, context, {
|
||||
FOCUS_CHATIN_FORMATTING: focusChainEnabled ? FOCUS_CHATIN_FORMATTING_TEMPLATE : "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.`
|
||||
|
||||
export async function getToolUseGuidelinesSection(_variant: PromptVariant, _context: SystemPromptContext): Promise<string> {
|
||||
return new TemplateEngine().resolve(TOOL_USE_GUIDELINES_TEMPLATE_TEXT, {})
|
||||
export async function getToolUseGuidelinesSection(_variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
return new TemplateEngine().resolve(TOOL_USE_GUIDELINES_TEMPLATE_TEXT, context, {})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user