Compare commits

..

74 Commits

Author SHA1 Message Date
celestial-vault e0a9510c1c using most up to date versions of these files 2025-09-22 19:03:26 -07:00
celestial-vault d03957f21d locking changes 2025-09-22 17:42:57 -07:00
Jose Castelli a9bc4c7d67 Testing platform coverage (#6332)
Testing platform coverage
2025-09-22 21:14:54 +02:00
celestial-vault 737dce09d1 Remove duplicate settings references in Task class (#6336)
* read settings from statemanager instead of keeping stale references in Task class and child classes

* removed unused vars
2025-09-22 12:02:17 -07:00
ZeroAurora a10d778bee fix: remove temperature settings in z.ai models (#6311)
To use the default temperature. (#6223 comment)
2025-09-22 12:00:13 -07:00
Jose Castelli 58b0ea9afa Run Testing platform within Test workflow [shadow] (#6273)
Run Testing platform within Test workflow [shadow]
2025-09-22 20:15:22 +02:00
canvrno 90fa3d7336 Dependency updates (#6096)
* Dependency updates

* package-lock

* Updated packages

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:39:11 -07:00
canvrno d0da22d5a9 Modify checkpoints to accept an array of workspaces (#6320)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:06:02 -07:00
nihar-oracle f2ce0b46a3 fix: Fixing oca provider utils (#6359) 2025-09-20 22:14:54 -07:00
github-actions[bot] 52ab767e44 v3.30.3 Release Notes (#6352)
* changeset version bump

* Updating CHANGELOG.md format

* Update version from 3.31.0 to 3.30.3

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 17:57:08 -07:00
Ara d858fb9360 Empty PR to bump changeset (#6351)
* Empty PR to bump changeset

* feat: Cleanup
2025-09-19 17:49:33 -07:00
nihar-oracle 428cdb670a feat(oca): add OCA provider with model picker and auth integration (#6339)
- Add webview UI components:

  - webview-ui/src/components/settings/providers/OcaProvider.tsx
  - webview-ui/src/components/settings/providers/OcaModelPicker.tsx

- Update settings to surface OCA:
  - webview-ui/src/components/settings/ApiOptions.tsx

- Wire backend for OCA auth and controller:

  - src/services/auth/oca/OcaAuthService.ts
  - src/services/auth/oca/providers/OcaAuthProvider.ts
  - src/core/controller/index.ts
2025-09-19 17:27:45 -07:00
Toshii aab002fe65 adding yolo mode telemetry which is triggered in grpc call updated (#6348) 2025-09-19 16:45:52 -07:00
Saoud Rizwan cc540d8158 Remove comment on cacheWritesPrice in clineCodeSupernovaModelInfo 2025-09-19 15:05:22 -07:00
Saoud Rizwan adc15c79d7 Fix copy 2025-09-19 14:57:54 -07:00
Saoud Rizwan af05d3497a v3.30.2 Release Notes 2025-09-19 14:51:19 -07:00
Saoud Rizwan 23fa1cb481 Fix Announcement banner UI tests 2025-09-19 14:50:33 -07:00
github-actions[bot] 800967d851 v3.30.1 Release Notes (#6343)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 14:25:10 -07:00
Saoud Rizwan 91deede3c3 fix: model list not being updated in time for user to use shortcut button to update model to stealth model (#6347)
* fix: model list not being updated in time for user to use shortcut button to update model to stealth model

* Create tame-rabbits-travel.md
2025-09-19 14:21:32 -07:00
pashpashpash b11e6171ff YOLO MODE (#6340)
* add yolo mode setting

* more explicit warning

* changeset
2025-09-19 13:43:50 -07:00
Bee 42666d9ca7 fix: use webview dependencies (#6344) 2025-09-19 13:40:32 -07:00
Bee 07f944b668 fix: update SectionHeader to prevent content overlap (#6337)
Remove sticky positioning and z-index styling from SectionHeader component to fix overlapping content issues during scroll. Also clean up unused imports and update description text styling to use semantic class.
2025-09-19 13:28:20 -07:00
canvrno c6e5b1509c Fix flicker issue when switching modes (#6341)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-19 13:04:29 -07:00
github-actions[bot] 6787dedf47 v3.30.0 Release Notes (#6338)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for Oracle Code Assist integration

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 12:04:04 -07:00
Saoud Rizwan d07648746a Add code-supernova stealth model (#6327)
* Update announcement banner

* Fix banner buttons

---------

Co-authored-by: pashpashpash <nik@cline.bot>
2025-09-19 11:53:07 -07:00
nihar-oracle 07b49baa2a feat: OCA provider (#5075)
* feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

update UI and add NPS survey link

papercuts

fix model dropdown height

fix: Fix 1

fix: removing ui

feat(AuthManager): Adding an AuthManager

fix: fixing rebase

* fix: Removing AuthManager, simplifyng auth service initialization
2025-09-19 00:33:24 -07:00
Alex Ker 2668bcdbe0 added baseten link to docs.json (#6312)
* added baseten link to docs.json

* fixed docs formatting

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-09-18 16:25:55 -07:00
github-actions[bot] e5e293c32b v3.29.2 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for v3.29.2 release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 15:56:16 -07:00
Bee fea8313695 Revert "fix: configure HeroUI setup (#6279)" (#6323)
* Revert "fix: configure HeroUI setup (#6279)"

This reverts commit 5c2d93617f.

* changeset
2025-09-18 15:44:01 -07:00
canvrno 4b450f4488 Pass max_tokens to moonshot provider (#6316)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 14:21:26 -07:00
Sarah Fortune 2687ae149f Set telemetry env vars for JetBrains builds [esbuild] (#6299) 2025-09-18 20:45:05 +00:00
celestial-vault 3b93871b50 use consolidated type parameters for getter functions in state-helpers (#6262)
* use consolidated type parameters for getter functions in state-helpers

* fix types

* fix types
2025-09-18 12:55:43 -07:00
Jose Castelli 767b81b22b Improve standalone startup times (#6272)
Improve standalone startup times
2025-09-18 21:38:46 +02:00
Jose Castelli 5db4970c7d Enhance Testing Framework - Improve non-deterministic scenarios + fix flag (#6244)
Enhance Testing Framework - Improve non-deterministic scenarios + fix flag
2025-09-18 21:27:41 +02:00
Sarah Fortune 4f66126a8c Consolidate duplicated code in BrowserSession and UrlContextFetcher (#6289)
Move duplicated code to utils.ts

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.
This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.
2025-09-18 11:40:44 -07:00
Sarah Fortune 43006ca401 Update runclinecore.sh script (#6300) 2025-09-18 10:12:56 -07:00
github-actions[bot] 1064c631c5 v3.29.1 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:15:56 -07:00
canvrno 981fe9cf09 Changeset bump and announcement update (#6290)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:01:36 -07:00
Sarah Fortune 90ab59d8f5 Replace context.globalStorageUri with the HostProvider in CheckpointTracker (#6277)
* Replace context.globalStorageUri with the HostProvider in CheckpointTracker

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.

Remove globalStoragePath param, it doesn't need to be passed around anymore.

Remove unused param taskId from shadowGit

* Remove globalStoragePath param from checkpoint manager.

Now tht the global storage path is available from the HostProvider anywhere, it doesn't need to be passed around.
2025-09-17 17:03:30 -07:00
kvyb f237dda413 fix: test e2e click “Show Code Actions” button then wait for listbox on macOS (#6287)
* fix: test e2e click “Show Code Actions” button then wait for listbox on macOS

* fix: trigger code actions during test with shortcut

* fix: add reasonable timeout to the menu and listbox call in test
2025-09-18 02:30:54 +03:00
Bee f08b0499d5 test: refactor chat e2e tests to support multi-roots workspace types (#6282)
* test: refactor chat e2e tests to support multiple workspace types

- Convert single test functions to parameterized test suite using describe blocks
- Add workspace type iteration for both chat messaging and slash command tests
- Consolidate test structure to run against different workspace configurations
- Maintain existing test logic while improving test coverage and organization
- consolidate chat input tests into single comprehensive test

Merged three separate chat input tests (slash commands, @ mentions, and partial completion) into one comprehensive test to reduce test setup time and improve test efficiency. Simplified test structure while maintaining all original functionality checks.

* update keybindings
2025-09-17 16:00:56 -07:00
Bee 5c2d93617f fix: configure HeroUI setup (#6279)
* fix: configure HeroUI setup

- Simplify Storybook stories pattern to only include TypeScript files
- Update HeroUI to v2.8.4 with dependencies
- Refactor HeroUI configuration with VSCode theme integration
- Streamline Tailwind CSS imports and plugin setup

* darkmode

* Theme

* remove workflow
2025-09-17 14:35:22 -07:00
celestial-vault 99201f9944 split up global state and settings types; split apart methods for fetching state and settings (#6263) 2025-09-17 14:11:56 -07:00
Sarah Fortune 0983a8a4b9 Replace context.globalStorageUri with HostProvider.globalStorageFsPath (#6276) 2025-09-17 21:05:10 +00:00
Jose Castelli 683096aed7 Interactive playwright script (#6222)
Interactive playwright script
2025-09-17 21:16:54 +02:00
github-actions[bot] 7189b224fc v3.29.0 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Updated changelog and announcement banner for release

* Swapped grok button for jetbrains button

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
2025-09-17 12:06:05 -07:00
Sarah Fortune fe3a75309d Don't use workspace resolver for tasks directory. (#6275) 2025-09-17 17:50:12 +00:00
Nick Baumann 7a00be2d55 Update JetBrains installation docs for GA release (#6264) 2025-09-17 10:39:50 -07:00
Ara 4e7af6eb5d Adding Multi Root Workspace Support for a few different tool calls (#6228)
* Adding multi root workspace support for different tools

* fixing spacing

* fixing spacing

* fixing spacing
2025-09-17 10:04:30 -07:00
Sarah Fortune e9c8f67822 Add global storage path to the Host Provider (#6248)
Add a the field `globalStorageFsPath` to replace the VSCode `context.globalStorageUri`
Remove duplicated code for getting the cache directory.
Add a util function to the HostProvider to get sub-directories of the global storage dir, and ensure that the sub-directory is created.
2025-09-17 00:13:12 -07:00
Sarah Fortune bef7d2c75a Clean up cline-core (#6255)
Move all the logic for waiting for the host bridge into `hostbridge-client.ts`
Add some comments
Use try/finally, remove unused param, log the error message when the health check fails.
2025-09-17 00:13:02 -07:00
Jose Castelli dfc660e73c Setup QLTY Initial Coverage Metrics (#6167)
Setup QLTY Initial Coverage Metrics
2025-09-17 08:39:08 +02:00
Bee af0bc4dc6c fix: CreditLimitError should use server returned url when available (#6261)
* fix: CreditLimitError should use server returned url when available

- Add useMemo hook for dashboard URL computation
- Display credit balance, spent, and promotions when available
- Replace inline styles with Tailwind CSS classes
- Simplify TaskServiceClient call by removing unnecessary properties
- Add proper conditional rendering for credit information
- Improve code organization and readability

* changeset added

* props

* Update storybook
2025-09-16 20:30:55 -07:00
Alex Ker f37962bbf2 Updated Baseten models to use dynamic fetching, added docs (#6148)
* Updated Baseten models to use dynamic fetching, added docs

* removed unused function, supportsImages

* added parsePrice into model-utils.ts

* updated toolcalling comment

* updated tools to fetch from API

* fixed conflicts and updated supportTools

* applied npm run format:fix

* added bedrock stuff back in that was breaking test

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-09-16 19:52:35 -07:00
celestial-vault e82597e069 move apihandler secrets to own type and fix some type errors related to the handlers (#6249) 2025-09-16 18:44:34 -07:00
Bee d1db8f747a docs: add comprehensive E2E testing documentation (#6259)
Add detailed documentation for end-to-end testing with Playwright including:
- Test execution commands and debug mode usage
- Test structure overview with file descriptions
- Writing test guidelines with fixtures and best practices
- Mock API server setup and workspace configuration
- Interactive debugging features and troubleshooting tips

Updates both CONTRIBUTING.md and E2E README to provide complete guidance for contributors working with the test suite.
2025-09-16 17:31:46 -07:00
Bee e8ba3c34fb feat: add Storybook configuration (#6256)
* feat: add Storybook configuration

- Bump package version from 3.28.3 to 3.28.4
- Add comprehensive Storybook setup with React-Vite framework and TypeScript support
- Configure Storybook with custom viewport settings and environment variables
- Add extensive story files for chat components, MCP displays, and browser automation
- Update gitignore files to exclude Storybook build artifacts and logs

* Add docs and changeset

* feat: refactor Storybook decorator to support state overrides and custom styling

- Extract ExtensionStateProviderWithOverrides component to safely use useExtensionState within provider context
- Add optional classNames parameter to createStorybookDecorator for custom styling
- Import cn utility from @heroui/react for className merging
- Fix margin class from m-x-auto to mx-auto

* Add message to mock history

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-09-16 17:27:55 -07:00
yuvalman fadcd7e3ad fix: sap provider - set sapAiCoreUseOrchestrationMode in state-helper instead of in sap provider components (#6221)
* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode

* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode

* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode
2025-09-16 14:57:30 -07:00
celestial-vault 81299d26d8 refactor: move favorited model IDs to dedicated state management (#6251)
Removed favorited_model_ids field from API configuration models in proto files and migrated storage handling to global state key management.

Cleans up configuration separation by removing non-API-related data from model definitions. Enables more consistent state handling across the application.
2025-09-16 14:56:14 -07:00
Sarah Fortune 832025c8cc Use the correct version number in cline-core (#6254)
* Use the correct version number in cline-core

cline-core should use the same version number as the extension because they share the same code for the core functionality.
Use the version number from the ExtensionRegistryInfo instead of from the standalone package.json

* Use node:fs instead of fs
2025-09-16 14:15:47 -07:00
celestial-vault f8a7a1cc58 [Cleanup] Remove API Configuration Conversion in updateSettings (#6196)
* Refactor API configuration to use proto enums and structured types

- Replace JSON string fields with proper proto message types
- Convert snake_case field names to camelCase for consistency
- Use ApiProvider enum instead of string for provider fields
- Add structured types for model info and selectors
- Remove deprecated settings conversion module
- Update proto conversions to handle new structured format

* fix types errors caused by changing to generic string
2025-09-16 10:15:32 -07:00
Sarah Fortune 3723288250 Add a field to the HostProvider for extension install dir (#6239)
* Add a field to the HostProvider `extensionFsPath`

Replace vscode.ExtensionContext `extensionUri` with `HostProvider.extensionFsPath`.

This part of removing uses of the VSCode API fom the codebase except for in platform specific code (these are src/hosts/vscode and src/extension.ts).

* Remove extra leading slash from webview URL on JetBrains

* Remove vscode.Uri from the WebviewProvider

Replace `vscode.Uri` with URL strings.
Replace Uri with Url in variable names.
2025-09-16 09:49:32 -07:00
Jose Castelli 0b5e8f5c37 Moving grpc recorder unit test (#6214)
Moving grpc recorder unit test
2025-09-16 12:54:39 +02:00
kvyb a04f6050ee fix standalone: isolate workspaceState per project via WORKSPACE_STORAGE_DIR (#6215)
* fix standalone: isolate workspaceState per project via WORKSPACE_STORAGE_DIR

* fix: keep logs global; default workspace storage dir; no try/catch on mkdir
2025-09-16 10:30:02 +03:00
Sarah Fortune ef7e8c5018 Use ExtensionRegistryInfo to get the extension version (#6237) 2025-09-16 04:06:43 +00:00
celestial-vault 60b17d092b Add task-specific settings caching to StateManager (#6171)
* Add task-specific settings caching to StateManager

Implement per-task settings storage with automatic loading/clearing:
- Add taskStateCache for isolated task settings
- Load task settings on task creation and reinitialize
- Clear task settings cache when tasks end
- Prioritize task cache over global state in getters

* update error wording for taskSettings read failure

* add missing return statement in promise.all

* fix: persist pending task settings before clearing cache

Changed clearTaskSettings to be async and ensure any pending task state
changes are persisted to storage before clearing the in-memory cache.
This prevents potential data loss when a task ends with unpersisted
settings still in the pending state queue.

- Made clearTaskSettings async and added optional taskId parameter
- Added logic to persist pending task state batch before clearing
- Updated controller to await clearTaskSettings and pass taskId
2025-09-15 15:49:10 -07:00
Bee 58808f3e2f fix process.env access for posthog (#6231)
* fix process.env access in build script

- Add CLINE_ENVIRONMENT=production to esbuild define config
- Remove fallback empty strings for API keys in build config
- Simplify optional chaining to direct property access for process.env
- Ensure consistent environment variable handling across config files

* verify CI secrets

* revert temp test
2025-09-15 13:55:34 -07:00
Sarah Fortune 281fd9505a Don't construct the IDE redirect URL in the webview. (#6229)
* Don't construct the redirect URL in the webview.

Add an RPC to the ProtoBus to get the URI to redirect back to the host IDE.
Support for JetBrains will be added in a second PR.

* Remove the uriScheme and extension name from the ExtensionStateContext.

These are being used to construct the IDE redirect URI, but this is not a cross-platform compatible way to do this.

* Update the host bridge to return the whole URL to redirect to the IDE

Return the whole redirect URI instead of just the URI scheme. VSCode needs the whole URI to redirect back to VSCode and open the cline extension.

* Update js doc

* Remove unused propertu uriScheme

All uses of vscode.env.uriScheme have been moved to platform specific code, so add it to linter rules for vscode API.
2025-09-15 12:58:25 -07:00
Ara fe1da0fa6c Normalize paths to fix windows test for workspace paths (#6227)
* Adding prompts

* Adding prompts
2025-09-15 12:17:49 -07:00
Toshii a505a79ec3 yolo mode (#6151)
* add global state var for yolo mode

* setting and updating yolo mode in autoApprover

* use approveAll variable in autoAPprove class

* add logic for automatically handling plan_mode_respond tool calls

* remove follow up question tool in yolo mode

* conditional parameter inclusion for execute_command in yolo mode

* logic for having terminal command continue in background once timer is up for yolo mode

* prompt adjustment for the timeout param

* changing function signature

* full template changes to support context

* merge conflict fix for system_info

* fixing default values

* update where we resolve template to TemplateEngine

* template engine test update

* test changes, adding types

* updating TemplateEngine test scoping

* add missing ide param in test

* clean up variable naming
2025-09-15 11:41:20 -07:00
Yukio Nozawa ac41b0dd33 Fix: Improve screen reader accessibility for MCP servers / Cline rules screens (#5861)
* Add descriptive aria-labels to mcp server screen

* Improve screen reader accessibility for cline rules page and its tabs

* Fix: Have screen readers read the new rule / new workflow file labels consistently (previously it always announced new rule file regardless of the actual file type)

* Add changeset
2025-09-15 11:40:41 -07:00
Yukio Nozawa fd3abdc68b fix: [accessibility] Improve screen reader accessibility for history view / preview (#5860)
* Fix: Add descriptive aria-labels to unlabelled buttons in the history preview screen

* Fix: Add descriptive aria-labels to unlabelled buttons in the history view screen

* Add changeset
2025-09-15 11:34:37 -07:00
Bee fe37ca9f29 Add environment variable injection to esbuild config (#6225)
* Add environment variable injection to esbuild config

Inject TELEMETRY_SERVICE_API_KEY, ERROR_SERVICE_API_KEY, and CLINE_ENVIRONMENT
at build time for production builds.

* add changeset

* update
2025-09-15 11:18:36 -07:00
Ara 7ecf395a9a Adding workspace adaptor and workspace hint for multi root workspace (#6143)
* Adding workspace adapter

* Adding workspace adapter
2025-09-15 10:10:33 -07:00
243 changed files with 10769 additions and 3015 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Checkpoints multiroot pt.1: Accept array of workspaces when initializting checkpoints
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Run Testing platform within Test workflow
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
remove temperature settings in z.ai models
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Interactive playwright script
+121 -2
View File
@@ -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
+2
View File
@@ -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/
+48
View File
@@ -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"
}
+6
View File
@@ -64,3 +64,9 @@ old_docs/**
e2e-build.mjs
e2e.vsix
test-results/
# Ignore Storybook files
**/*.stories.tsx
*storybook.log
storybook-static
**/StorybookDecorator.tsx
+34
View File
@@ -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
+30
View File
@@ -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
View File
@@ -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.
+21 -5
View File
@@ -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:
+115
View File
@@ -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
View File
@@ -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
+72 -22
View File
@@ -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
View File
@@ -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",
+412 -219
View File
File diff suppressed because it is too large Load Diff
+46 -8
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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 {
+52 -3
View File
@@ -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;
}
+36
View File
@@ -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
View File
@@ -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.
+4 -8
View File
@@ -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;
}
+112
View File
@@ -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)
})
+3 -1
View File
@@ -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
+40 -35
View 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)
})
}
+102 -36
View File
@@ -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)
}
+8 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 = {
+6 -11
View File
@@ -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
}
}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
}
+8 -3
View File
@@ -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 || ""
+1
View File
@@ -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 },
})
+261
View File
@@ -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,
}
}
}
+1 -1
View File
@@ -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)
-1
View File
@@ -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
+1
View File
@@ -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
+3 -2
View File
@@ -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)
+2 -2
View File
@@ -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({
+1 -1
View File
@@ -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()
@@ -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
View File
@@ -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()
}
+35 -27
View File
@@ -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 }
}
+4 -4
View File
@@ -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 })
+229
View File
@@ -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()
}
}
+28
View File
@@ -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