Compare commits

...

55 Commits

Author SHA1 Message Date
celestial-vault 2f2f7f2a1b merge conflicts 2025-06-06 18:58:23 -07:00
Andrei Eternal 4d7ced7e15 [Protobus] migrate executeQuickWin (#4088)
* protobuf executeQuickWin

* whitespaces

---------

Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-06 18:46:31 -07:00
canvrno d7e1fff011 Task init promise error in task truncation (#4097) 2025-06-06 17:28:36 -07:00
Evan 8ca96dfd9f remove proto gen files (#4045)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-06 13:34:53 -07:00
canvrno 8c559d6916 one small fix (#4086) 2025-06-06 12:59:26 -07:00
canvrno 2158165fc4 [PROTOBUS] updateSettings & didUpdateSettings (#4073)
* updateSettings proto mvp

* changeset
2025-06-06 12:43:03 -07:00
canvrno 78792c240f Spring cleaning (#3723)
* Spring cleaning

* changeset

* Fixed incorrect return on ScrollToSettings proto

* cleanup
2025-06-06 11:50:59 -07:00
Sarah Fortune 0abd13e1f2 Better environments for the vibes (#4077)
* Get root of the cline storage directory from an env var.

Set the cline directory env var when running the standalone app from the vscode launcher.
Add missing vscode SDK stub.

* Stop spamming the logs.
2025-06-06 10:15:29 -07:00
Beatrix 7e37cec1ab Fix hardcoded context menu index for File option selection (#4065)
* Fix hardcoded context menu index for File option selection

The ChatTextArea component was using a hardcoded index 3 to select the "File" option by default in the context menu, but this was incorrect - the File option is actually at index 5 in the menu options array. This caused the wrong option (Git) to be selected by default when pressing Escape or when no query is provided.

Additionally, the hardcoded approach was fragile and would break if the context menu order changed in the future.

Key changes:

- Added `DEFAULT_CONTEXT_MENU_OPTIONS` array in context-mentions.ts to define the canonical menu order
- Added helper function `getDefaultContextMenuOptionIndex()` dynamically finds the correct index for any option type
- Updated ChatTextArea to use `DEFAULT_CONTEXT_MENU_OPTION` constant instead of hardcoded 3
- Updated `getContextMenuOptions()` to use the new centralized array

* changeset
2025-06-06 00:44:24 -07:00
canvrno b0926b1647 Fixed telemetry popup bug (#4070) 2025-06-05 22:06:24 -07:00
Alberto Valiña Lema 582a3b190d Collapsible Panel for MCP Responses (#3528)
* feat: the response of the mcps is represented by a collapsible

* feat: the response of the mcps is represented by a collapsible

* fix: message for error parsing response

* fix: format

* feat: added cline.mcp.defaultPanelState settings

* chore: merge

* feat: improve global state

* chore: re-trigger  workflow

* chore: lint fix

* revert: unnecessary changes

* Update webview-ui/src/components/mcp/chat-display/McpResponseDisplay.tsx

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>

* chore: change MCP Response Display Mode to a button

* fix: improve ui mcp response panel

* refactor: change name and type for property mcpDefaultPanelState to mcpResponsesCollapsed

* chore: rename props

---------

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>
2025-06-05 17:14:43 -07:00
Evan 8c8a398f90 Migrate webviewDidLaunch protobus (#4063)
* migrate openRouterModels

* remove conversion functions and unused imports

* migrate webviewDidLaunch

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 13:47:53 -10:00
Evan 742a72b4ec migrate openRouterModels protobus (#4058)
* migrate openRouterModels

* remove conversion functions and unused imports

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 15:47:11 -07:00
Sarah Fortune 84a00b05e0 Add better stubs for the workspace. (#4061)
The extension doesn't like it when the workspace/workspace config is undefined.
Fix the warning about the name in package.json.
Change some logging in the gRPC server.
2025-06-05 15:19:09 -07:00
Evan e21fa3fff9 migrate theme message protobus (#4012)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 13:12:19 -07:00
Beatrix 5b3647fdff Add active files to file context menu (#4048)
* Add active files to file context menu

Add support to include active files (open tabs) within the WorkspaceTracker. This enables the tracker to send the webview with a file list that has active files listed at the top.

The changes include:

- Listening for tab group changes using `vscode.window.tabGroups.onDidChangeTabs` to trigger workspace updates.
- Introducing an `activeFiles` getter that retrieves the file paths of all currently open text editor tabs.
- Modifying the `workspaceDidUpdate` function to include both `activeFiles` and `filePaths` when posting the `workspaceUpdated` message to the webview.

* use set

* add changeset
2025-06-06 00:49:17 +05:30
github-actions[bot] 14b6e71416 v3.17.11 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and version for 3.17.11 patch release - Add Gemini 2.5 Pro Preview 06-05

---------

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: pashpashpash <nik@nugbase.com>
2025-06-05 11:51:40 -07:00
Ara 4869aba90f Adding Gemini 2.5 pro preview 06-05 (#4053) 2025-06-05 11:37:06 -07:00
Evan c68e427d13 migrate mcpMarketplaceCatalog (#4023)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:17:13 -07:00
Evan 0dca4dedbd migrate partialMessage (#4033)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:02:37 -07:00
Evan c6e7b5249e Migrate settingsButtonClicked protobus (#3976)
* migrate settingsButtonClicked

* changeset

* add provider type filtering

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 20:49:07 -07:00
github-actions[bot] 9dcb82f22e v3.17.10 Release Notes
v3.17.10 Release Notes
2025-06-04 19:05:11 -07:00
Toshii fa8491b3aa remove log (#4040) 2025-06-04 18:53:12 -07:00
pashpashpash b811db823b Option to disable aggressive terminal reuse for users affected by the task lockout bug. (#4041)
* terminal setting for reusing terminal commands

* language

* changeset

* changeset language

* comment clarity

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-04 18:52:53 -07:00
Sarah Fortune 4f7dc6fb36 Add a postMessage handler for standalone cline app (#4038)
* Add a postMessage handler for standalone cline app.

If there is no vscode api defined, try to use the standalone post
Message handler.

* Imports

* Update logs
2025-06-04 17:44:38 -07:00
Sarah Fortune b7096a77db Add type checking for protobus RPC handlers. (#4020)
* Add type checking for protobus RPC handlers.

Add type parameters to the wrapper function when generating the server-setup file.
So that the parameters of the RPC handler will be type checked against types defined in the service.
The generated code looks like this:
```
    server.addService(proto.cline.CheckpointsService.service, {
         checkpointDiff: wrapper<cline.Int64Request,cline.Empty>(checkpointDiff, controller),
         checkpointRestore: wrapper<cline.CheckpointRestoreRequest,cline.Empty>(checkpointRestore, controller),
    });
```

Add an index.ts file to the proto directory so all the proto types can be imported without having to know which proto message is defined in which file (the proto descriptor set doesn't have this information).

* Generate index.ts with protoc instead of doing it ourselves.

Turn on the option 'dontExportCommonSymbols' in protoc, so that each generated proto file is not trying to export the same utility functions.

Generate all the protos with the same protoc command.

* cleanup
2025-06-04 17:42:19 -07:00
Andrei Eternal cbcd17764b Protobus Host Bridge (#3747)
* WIP host bridge

* Run formatter

* remove tmp impl & rename host grpc client

* gitignore more files

* better layout

* host handler to make other hosts easier to add

* remove adapter pattern

* get host responses correctly

* fix streaming mode for host bridge

* first wip subscription host bridge demo for watching mcp server config

* format, comment

* add cancellation for host grpc stream

* remove unneeded functions from host-grpc-handler

* add a method for canceling request rather than using the registry

* another todo

* use StringRequest for uri.proto

* debounce new file watcher

* remove test setup

* remove some todos and logs

* remove registry use todo

* Revert "remove registry use todo"

This reverts commit 84078d3469.

* fix capitalization of uri.proto

* a better pattern for a callback based bridge without using the requestRegistry directly

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-04 15:41:30 -07:00
Ara 47899ac52d Adding Cline Walkthrough (#3746)
* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* New files

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-04 13:45:52 -07:00
Evan 586eb3cd9e migrate accountButtonClicked (#4008)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 11:40:52 -07:00
Steven White 584535393f New AskSage Models (#3780)
* chore: new AskSage models

* chore: add changeset
2025-06-04 11:21:18 -07:00
Jonny Chen 01a9ab0cf6 feat: add all qwen3 models support and add thinking mode options (#3692)
* feat: add all qwen3 models support and add thinking mode options

* fix: qwen model reasoning logic configuration, set the qwen model maxBudget value

* fix: correct the maxTokens in qwen3 model

* vertex model

---------

Co-authored-by: xuanqi <xuanqi.cc@alibaba-inc.com>
Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-06-04 09:08:48 -07:00
Sarah Fortune 51a538373f Make the protobuf object literal lint check an error instead of a warning. (#4019)
Fix remaining occurences of protobuf literals.
2025-06-03 23:58:52 -07:00
canvrno a6ce75d17c Telemetry fix (#4009) 2025-06-03 18:44:52 -07:00
Elephant Lumps 70e8d8bc13 fix linter warning about empty proto object creation 2025-06-03 16:32:46 -07:00
Elephant Lumps 9b3d93d4d0 migrate workspaceUpdated 2025-06-03 16:26:31 -07:00
Evan 0a3d0e3569 Migrate chatbuttonclicked protobus (#3854)
* migrate chatButtonClicked

* changeset

* send targeted event to the controller

* prettier

* add test mock

* continue to fix tests

* fix test mocks once and for all

* fix test mocks once and for all

* try again please lord

* try again

* temporarily disable tests

* revert other test 'fixes'

* one more

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-03 14:47:08 -07:00
Sarah Fortune fccac277d2 feat(lint): Add custom ESLint rule for checking that protobuf messages are created using .create() in more contexts (#3929)
* feat(lint): Add custom ESLint rules for protobuf type checking

Add two custom ESLint rules to enforce proper usage patterns when creating protobuf objects.

Using .create() to build protobufs ensures that the protobuf is type checked when it is created. Protobufs created using
object literals are not type checked,  which can lead to subtle bugs and type mismatches. The linter rules detect when protobufs are created without using .create() or .fromPartial().

- no-protobuf-object-literals: Enforces the use of `.create()` or `.fromPartial()` methods instead of object literals when creating protobuf types.

```
/Users/sjf/cline/src/shared/proto-conversions/state/chat-settings-conversion.ts
   9:9  warning  Use ChatSettings.create() or ChatSettings.fromPartial() instead of object literal for protobuf type
Found: return {
             mode: chatSettings.mode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
             preferredLanguage: chatSettings.preferredLanguage,
             openAiReasoningEffort: chatSettings.openAIReasoningEffort,
     }
  Suggestion: ChatSettings.create({
             mode: chatSettings.mode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
             preferredLanguage: chatSettings.preferredLanguage,
             openAiReasoningEffort: chatSettings.openAIReasoningEffort,
     })
```

- no-grpc-client-object-literals: Enforces proper protobuf creation for gRPC service client parameters. This needs a separate rule
because the type signatures of the ServiceClients methods are too generic to be detected by the previous rule.

```
/Users/sjf/cline/webview-ui/src/components/mcp/configuration/tabs/add-server/AddRemoteServerForm.tsx
   41:62  warning  Use the appropriate protobuf .create() or .fromPartial() method instead of object literal for gRPC client parameters.
Found: McpServiceClient.addRemoteMcpServer({
                             serverName: serverName.trim(),
                             serverUrl: serverUrl.trim(),
                     })
```

These rules help maintain code quality by enforcing consistent patterns for working with protocol buffers throughout the codebase, reducing potential runtime errors from improper message construction.

* Update test

* Add custom eslint rules to new webview-ui config

* formatting

* stuff

* undo protobuf fixes

* update rule

* update rule

* protobuf fixes

* rename unused params

* formatting
2025-06-03 14:21:54 -07:00
Paul Gear 00901e01b7 Change Ollama model selector to filterable dropdown (#3999)
* use dropdown for Ollama model list when possible

Code changes by Qwen3 30B A3B, based on OpenRouterModelPicker

* Document libasound2 and libnss3 test dependencies, sort list

* add test for OllamaModelPicker

Code by Claude Sonnet 3.7

* Add changeset
2025-06-03 13:52:50 -07:00
Sarah Fortune 4a3f5c4f69 Add globalState to the standalone vscode extension context replacement. (#3987)
* Add globalState to the standalone vscode extension context replacement.

Add a generic key-value store that can be used by different storages, and move this into vscode-context-utils file.
Move the stubs/mocks into a separate stubs file, (they have type checking turned off).
Keep the implementations in vscode-context and turn on typechecking for this file.

* Add type parameter for the values in the JsonKeyValueStore

* Optimize imports

* Add implementations for the vscode ExtensionContext in the standalone app (#4000)

* Formatting

* add package-lock.json
2025-06-03 12:06:13 -07:00
Michael 9f85105186 Add retry logic to API handlers with @withRetry decorator (#3596) 2025-06-03 10:19:14 -07:00
Isabel Zimmerman a9e846211b remove hanging bullet point (#3989) 2025-06-03 01:04:12 -07:00
Ara 8cbf25c026 Adding support for Streamable MCP server (#3970)
* Fixing OpenAI compatible to support cache token display

* Removing redundant logging and write to file

* Removing redundant logging and write to file

* Splitting constants

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-03 00:29:45 -07:00
francis e5857cbfec Docs: Update docs.json and formatting (#3991)
* docs: add xAI Grok and Mistral AI provider configs

* docs: add Anthropic Claude model configuration guide

- Add comprehensive documentation for configuring Anthropic Claude models with Cline
- Include API key setup, supported models list, and configuration steps
- Cover advanced features like prompt caching and rate limits
- Update navigation to include new Anthropic page in custom model configs section

* docs: add DeepSeek, Ollama, OpenAI, OpenAI Compatible pages and update Plan & Act

* docs: add Extended Thinking section to Anthropic configuration guide

* docs: update vscode language model api page

* docs: update vscode language model api docs

* Add model documentation pages and update navigation structure

- Add new documentation pages for model overviews (Claude, Gemini, OpenAI, XAI)
- Add general models overview page
- Update docs.json to include new model documentation in navigation
- Update OpenAI-compatible model documentation

* Remove Notes column from model documentation tables for consistency

* Fix table formatting in Gemini models documentation

* added 5 new model configurations and updated existing ones

* Update AWS Bedrock documentation with minimal IAM permissions

* modified:   docs/get-to-know-the-models/claude-models.mdx

* Renamed 'custom model configuration' to 'provider configuation' to avoid providers being confused with models

* Fix dollar sign rendering in model documentation

- Escape dollar signs in pricing tables to prevent MDX parsing issues
- Fixes disappearing dollar signs in gemini-models.mdx and other model docs
- Dollar signs now display correctly as literal currency symbols

* Add feature descriptions to OpenAI and XAI model docs

- Added 'Diverse Performance for Different Tasks Across Model Tiers' section to OpenAI models
- Added 'Real-time Information Access' section to XAI models
- Maintains consistency with existing Claude and Gemini documentation format
- Highlights valuable features for agentic AI coding workflows

* removing these files due to name change in header. they're in the new docs/provider configs folder

* added 2 new issues per model page + formatting

* Update model documentation files

* Fix: Correct paths in docs.json for provider configs

* docs: update docs.json and apply formatting

* docs: fix broken links, add OpenRouter & Requesty pages

* docs: remove 'get to know the models' section and files

* Update docs/provider-config/openai-compatible.mdx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: kevinneung <94151024+kevinneung@users.noreply.github.com>
Co-authored-by: Dennise Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-02 20:50:49 -07:00
github-actions[bot] 064dac48f8 v3.17.9 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and version for patch release 3.17.9

- Change version from 3.18.0 to 3.17.9 (patch release)
- Update CHANGELOG.md with user-friendly descriptions
- Add proper attribution for external contributors
- Focus on user-facing changes and bug fixes
- Remove internal/dev-only changes from changelog

* added claude 4 stuff

---------

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: pashpashpash <nik@nugbase.com>
2025-06-02 19:45:45 -07:00
pashpashpash 82d0ac2088 Cozy Claude 4 (#3995)
* making claude4 feel better

* system prompt whoops

* small system prompt change

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-02 19:28:35 -07:00
David Nanyan 3a1cee2faf Add stale workflow (#3672) 2025-06-02 19:02:11 -07:00
canvrno 8171b887ad Fix for checkpoints (#3993) 2025-06-02 18:25:44 -07:00
pashpashpash 203f805548 Claude 4 - experimental flag - defaults to classic function calling with some minor changes to system prompt (#3994)
* adding modularized flag for new claude4 experimental tools, default OFF

* diff.ts

* responses.ts

* system.ts

* tests and prompt

* forgot some chars

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-02 18:22:38 -07:00
Nigel Packer c91ec4c97d Remove hard-coded temperature from LM Studio API requests and add support for easoning_content in LM Studio API responses. (#3971) 2025-06-02 18:18:13 -07:00
Evan d85c8ceeb2 migrate historyButtonClicked to protobus (#3977)
* migrate historyButtonClicked

* add webview provider type filtering

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-02 15:23:19 -07:00
Evan b38994f1e8 Migrate mcpButtonClicked protobus (#3975)
* migrate mcpButtonClicked

* changeset

* only send event to matching webview type

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-02 12:38:30 -07:00
Ara d846c2cce9 Removing redundant logging and write to file (#3981)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-02 08:58:13 +05:30
Adel Khial 6e50778db7 Add DeepSeek-R1-0528 to Nebius AI Studio API (#3973)
* Add DeepSeek-R1-0528 to Nebius AI Studio model list

* Add changeset

* Fix max tokens
2025-06-01 16:17:10 -07:00
Evan 3a325a6445 Open the hood (#3949)
* add open disk conversation history button

* changeset

* change icon due to lack of artistic freedom

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-01 14:51:07 -07:00
Evan f73172aeae update bedrock sdk (#3978)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-01 14:18:07 -07:00
172 changed files with 12206 additions and 5980 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add models to vertex ai
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Update `xaiModels` object and `xaiDefaultModelId` in `src/shared/api.ts`
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Migrated updateSettings to protos, removed didUpdateSettings, altered Plan/Act toggling in settings menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fixing token counting for xai provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed issue where telemetry warning popup was created for every new Cline window
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Prioritize active files in file context menu
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix(bedrock): Use ignoreCache for profile-based AWS credential loading
Ensures that AWS Bedrock provider always fetches fresh credentials when using IAM profiles by setting `ignoreCache: true` for `fromNodeProviderChain`. This resolves issues where externally updated credentials (e.g., by AWS Identity Manager) were not detected by Cline, requiring an extension restart. Manual credential handling remains unchanged.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add suppport for parsing csv and xlsx
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed search tool overloading conversation with massive outputs by setting a maximum overall byte limit for search tool responses
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added promise to task init to prevent race condition with checkpoints
@@ -2,4 +2,4 @@
"claude-dev": patch
---
change proto type
Spring cleaning
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
update chat box ui
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
bug fix for ollama
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
update the openrouter model list when refreshing
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Title for windows in cline
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
scroll to task timeline
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add grep tool with new parsing format
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Context menu is default to File option on start up
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
optionsResponse protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Display delay information when retrying API calls
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Pass type of webview (tab or sidebar) to webview so it knows what type it is
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
the response of the mcps is displayed with a collapsible which allows to focus on the model responses.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add edit tool definition
+1
View File
@@ -20,6 +20,7 @@
"no-throw-literal": "warn",
"semi": "off",
"react-hooks/exhaustive-deps": "off",
"eslint-rules/no-protobuf-object-literals": "error",
"eslint-rules/no-grpc-client-object-literals": "error"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
+25
View File
@@ -0,0 +1,25 @@
# This workflow will only label and/or close 30 issues at a time in order to avoid exceeding a rate limit.
# More info: https://docs.github.com/en/actions/use-cases-and-examples/project-management/closing-inactive-issues
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@28ca103
with:
days-before-issue-stale: 60
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "pinned,security"
repo-token: ${{ secrets.GITHUB_TOKEN }}
+32
View File
@@ -0,0 +1,32 @@
name: Test Stale Issues Workflow
on:
workflow_dispatch:
inputs:
days-before-stale:
description: "Days before an issue becomes stale"
required: true
default: "1"
days-before-close:
description: "Days before a stale issue is closed"
required: true
default: "1"
jobs:
test-stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@28ca103
with:
days-before-issue-stale: ${{ github.event.inputs.days-before-stale }}
days-before-issue-close: ${{ github.event.inputs.days-before-close }}
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for ${{ github.event.inputs.days-before-stale }} days with no activity."
close-issue-message: "This issue was closed because it has been inactive for ${{ github.event.inputs.days-before-close }} days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "pinned,security"
repo-token: ${{ secrets.GITHUB_TOKEN }}
debug-only: true
+3 -2
View File
@@ -86,8 +86,9 @@ jobs:
- name: Build Tests and Extension
run: npm run pretest
- name: Unit Tests
run: npm run test:unit
# Unit Tests disabled due to module system conflicts between backend and webview-ui
# - name: Unit Tests
# run: npm run test:unit
# Run extension tests with coverage
- name: Extension Tests with Coverage
+10 -1
View File
@@ -22,9 +22,18 @@ coverage
*evals.env
# Generated proto files
src/shared/proto/*.ts
src/core/controller/*/methods.ts
src/core/controller/*/index.ts
src/core/controller/grpc-service-config.ts
# Shared
src/shared/proto/*.ts
src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Standalone
src/standalone/server-setup.ts
src/standalone/services/host-grpc-client.ts
# Host bridge
hosts/vscode/*/methods.ts
hosts/vscode/*/index.ts
hosts/vscode/host-grpc-service-config.ts
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/__tests__/*.ts",
"spec": ["src/**/__tests__/*.ts", "eslint-rules/__tests__/**/*.test.ts"],
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
"recursive": true
}
+2 -1
View File
@@ -52,7 +52,8 @@
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"CLINE_DIR": "${userHome}/.cline-standalone"
},
"program": "standalone.js"
}
+32
View File
@@ -1,5 +1,37 @@
# Changelog
## [3.17.11]
- Add support for Gemini 2.5 Pro Preview 06-05 model to Vertex AI and Google Gemini providers
## [3.17.10]
- Add support for Qwen 3 series models with thinking mode options (Thanks @Jonny-china!)
- Add new AskSage models: Claude 4 Sonnet, Claude 4 Opus, GPT 4.1, Gemini 2.5 Pro (Thanks @swhite24!)
- Add VSCode walkthrough to help new users get started with Cline
- Add support for streamable MCP servers
- Improve Ollama model selection with filterable dropdown instead of radio buttons (Thanks @paulgear!)
- Add setting to disable aggressive terminal reuse to help users experiencing task lockout issues
- Fix settings dialog applying changes even when cancel button is clicked
## [3.17.9]
- Aligning Cline to work with Claude 4 model family (Experimental)
- Add task timeline scrolling feature
- Add support for uploading CSV and XLSX files for data analysis and processing
- Add stable Grok-3 models to xAI provider (grok-3, grok-3-fast, grok-3-mini, grok-3-mini-fast) and update default model from grok-3-beta to grok-3 (Thanks @PeterDaveHello!)
- Add new models to Vertex AI provider
- Add new model to Nebius AI Studio
- Remove hard-coded temperature from LM Studio API requests and add support for reasoning_content in LM Studio responses
- Display delay information when retrying API calls for better user feedback
- Fix AWS Bedrock credential caching issue where externally updated credentials (e.g., by AWS Identity Manager) were not detected, requiring extension restart (Thanks @DaveFres!)
- Fix search tool overloading conversation with massive outputs by setting maximum byte limit for responses
- Fix checkpoints functionality
- Fix token counting for xAI provider
- Fix Ollama provider issues
- Fix window title display for Windows users
- Improve chat box UI
## [3.17.8]
- Fix bug where terminal would get stuck and output "capture failure"
+23 -9
View File
@@ -34,18 +34,20 @@ If you're planning to work on a bigger feature, please create a [feature request
3. **Linux-specific Setup**
VS Code extension tests on Linux require the following system libraries:
- `libatk1.0-0`
- `dbus`
- `libasound2`
- `libatk-bridge2.0-0`
- `libxkbfile1`
- `libatk1.0-0`
- `libdrm2`
- `libgbm1`
- `libgtk-3-0`
- `libnss3`
- `libx11-xcb1`
- `libxcomposite1`
- `libxdamage1`
- `libxfixes3`
- `libxkbfile1`
- `libxrandr2`
- `libgbm1`
- `libdrm2`
- `libgtk-3-0`
- `dbus`
- `xvfb`
These libraries provide necessary GUI components and system services for the test environment.
@@ -54,9 +56,21 @@ If you're planning to work on a bigger feature, please create a [feature request
```bash
sudo apt update
sudo apt install -y \
libatk1.0-0 libatk-bridge2.0-0 libxkbfile1 libx11-xcb1 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 \
libdrm2 libgtk-3-0 dbus xvfb
dbus \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbfile1 \
libxrandr2 \
xvfb
```
- Run `npm run test:ci` to run tests locally
+15 -6
View File
@@ -143,13 +143,22 @@
]
},
{
"group": "Custom Model Configurations",
"group": "Provider Configuration",
"pages": [
"custom-model-configs/aws-bedrock-with-credentials-authentication",
"custom-model-configs/aws-bedrock-with-profile-authentication",
"custom-model-configs/gcp-vertex-ai",
"custom-model-configs/litellm-and-cline-using-codestral",
"custom-model-configs/vscode-language-model-api"
"provider-config/anthropic",
"provider-config/aws-bedrock-with-credentials-authentication",
"provider-config/aws-bedrock-with-profile-authentication",
"provider-config/gcp-vertex-ai",
"provider-config/litellm-and-cline-using-codestral",
"provider-config/vscode-language-model-api",
"provider-config/xai-grok",
"provider-config/mistral-ai",
"provider-config/deepseek",
"provider-config/ollama",
"provider-config/openai",
"provider-config/openai-compatible",
"provider-config/openrouter",
"provider-config/requesty"
]
},
{
@@ -14,9 +14,9 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock-with-credentials-authentication.mdx)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-with-profile-authentication.mdx)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
#### VPC Endpoint Setup
+7 -4
View File
@@ -6,10 +6,13 @@ sidebarTitle: "Plan & Act"
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/planningThenActing%20(1).gif"
alt="Use Plan to gather context before using Act to implement the plan"
/>
<iframe
style={{ width: "100%", aspectRatio: "16/9" }}
src="https://www.youtube.com/embed/b7o6URFPp64"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen></iframe>
</Frame>
#### Plan Mode: Think First
@@ -60,7 +60,6 @@ Now that you have Cline installed, let's get you set up with your account:
- DeepSeek Chat (cost-effective alternative)
- Google Gemini 2.0 Flash
- And more — all through your Cline account.
4. -
### 💻 Your First Interaction with Cline
+61
View File
@@ -0,0 +1,61 @@
---
title: "Anthropic"
description: "Learn how to configure and use Anthropic Claude models with Cline. Covers API key setup, model selection, and advanced features like prompt caching."
---
**Website:** [https://www.anthropic.com/](https://www.anthropic.com/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [Anthropic Console](https://console.anthropic.com/). Create an account or sign in.
2. **Navigate to API Keys:** Go to the [API keys](https://console.anthropic.com/settings/keys) section.
3. **Create a Key:** Click "Create Key". Give your key a descriptive name (e.g., "Cline").
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
### Supported Models
Cline supports the following Anthropic Claude models:
- `claude-opus-4-20250514`
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
- `claude-sonnet-4-20250514` (Recommended)
- `claude-sonnet-4-20250514:thinking` (Extended Thinking variant)
- `claude-3-7-sonnet-20250219`
- `claude-3-7-sonnet-20250219:thinking` (Extended Thinking variant)
- `claude-3-5-sonnet-20241022`
- `claude-3-5-haiku-20241022`
- `claude-3-opus-20240229`
- `claude-3-haiku-20240307`
See [Anthropic's Model Documentation](https://docs.anthropic.com/en/docs/about-claude/models) for more details on each model's capabilities.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Anthropic" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Anthropic API key into the "Anthropic API Key" field.
4. **Select Model:** Choose your desired Claude model from the "Model" dropdown.
5. **(Optional) Custom Base URL:** If you need to use a custom base URL for the Anthropic API, check "Use custom base URL" and enter the URL. Most users won't need to adjust this setting.
### Extended Thinking
Anthropic models offer an "Extended Thinking" feature, designed to give them enhanced reasoning capabilities for complex tasks. This feature allows the model to output its step-by-step thought process before delivering a final answer, providing transparency and enabling more thorough analysis for challenging prompts.
When extended thinking is in Cline, the model generates `thinking` content blocks that detail its internal reasoning. These insights are then incorporated into its final response.
Cline users can leverage this by checking the `Enable Extended Thinking` box below the model selection menu after selecting a Claude Model from any provider.
**Key Aspects of Extended Thinking:**
- **Supported Models:** This feature is available for select models, including variants of Claude Opus 4, Claude Sonnet 4, and Claude Sonnet 3.7. The specific models listed in the "Supported Models" section above with the `:thinking` suffix are pre-configured in Cline to utilize this.
- **Summarized Thinking (Claude 4):** For Claude 4 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
- **Streaming:** Extended thinking responses, including the `thinking` blocks, can be streamed.
- **Tool Use & Prompt Caching:** Extended thinking interacts with tool use (requiring thinking blocks to be passed back) and prompt caching (with specific behaviors around cache invalidation and context).
For comprehensive details on how extended thinking works, including API examples, interaction with tool use, prompt caching, and pricing, please refer to the [official Anthropic documentation on Extended Thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking).
### Tips and Notes
- **Prompt Caching:** Claude 3 models support [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching), which can significantly reduce costs and latency for repeated prompts.
- **Context Window:** Claude models have large context windows (200,000 tokens), allowing you to include a significant amount of code and context in your prompts.
- **Pricing:** Refer to the [Anthropic Pricing](https://www.anthropic.com/pricing) page for the latest pricing information.
- **Rate Limits:** Anthropic has strict rate limits based on [usage tiers](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). If you're repeatedly hitting rate limits, consider contacting Anthropic sales or accessing Claude through a different provider like [OpenRouter](/provider-config/openrouter) or [Requesty](/provider-config/requesty).
@@ -25,12 +25,41 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
#### 1.2 Attach the Required Policies
1. **Attach the Managed Policy:**
- Attach the **`AmazonBedrockFullAccess`** managed policy to your user/role.\
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
2. **Confirm Additional Permissions:**
- Ensure your policy includes permissions for model invocation (e.g., `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`), model listing, and AWS Marketplace actions (like `aws-marketplace:Subscribe`).
- _Enterprise Tip:_ Apply least-privilege practices by scoping resource ARNs and using [Service Control Policies (SCPs)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html) to restrict access where necessary.
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
**Option 1: Minimal Permissions (Recommended for Production & Least Privilege)**
1. In the AWS IAM console, create a new policy.
2. Use the JSON editor to add the following policy document:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
}
]
}
```
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to your IAM user or role.
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
**Important Considerations:**
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), ensure you have active AWS Marketplace subscriptions. This is typically managed in the AWS Bedrock console under "Model access" and might require `aws-marketplace:Subscribe` permissions if not already handled.
- _Enterprise Tip:_ Always apply least-privilege practices. Where possible, scope resource ARNs in your IAM policies to specific models or regions. Utilize [Service Control Policies (SCPs)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html) for overarching governance in AWS Organizations.
---
+33
View File
@@ -0,0 +1,33 @@
---
title: "DeepSeek"
description: "Learn how to configure and use DeepSeek models like deepseek-chat and deepseek-reasoner with Cline."
---
Cline supports accessing models through the DeepSeek API, including `deepseek-chat` and `deepseek-reasoner`.
**Website:** [https://platform.deepseek.com/](https://platform.deepseek.com/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [DeepSeek Platform](https://platform.deepseek.com/). Create an account or sign in.
2. **Navigate to API Keys:** Find your API keys in the [API keys](https://platform.deepseek.com/api_keys) section of the platform.
3. **Create a Key:** Click "Create new API key". Give your key a descriptive name (e.g., "Cline").
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
### Supported Models
Cline supports the following DeepSeek models:
- `deepseek-v3-0324` (Recommended for coding tasks)
- `deepseek-r1` (Recommended for reasoning tasks)
### Configuration in Cline
1. **Open Cline Settings:** Click the ⚙️ icon in the Cline panel.
2. **Select Provider:** Choose "DeepSeek" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your DeepSeek API key into the "DeepSeek API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
### Tips and Notes
- **Pricing:** Refer to the [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing/) page for details on model costs.
+53
View File
@@ -0,0 +1,53 @@
---
title: "Mistral"
description: "Learn how to configure and use Mistral AI models, including Codestral, with Cline. Covers API key setup and model selection."
---
Cline supports accessing models through the Mistral AI API, including both standard Mistral models and the code-specialized Codestral model.
**Website:** [https://mistral.ai/](https://mistral.ai/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [Mistral Platform](https://console.mistral.ai/). Create an account or sign in. You may need to go through a verification process.
2. **Create an API Key:**
- [La Plateforme API Key](https://console.mistral.ai/api-keys/) and/or
- [Codestral API Key](https://console.mistral.ai/codestral)
### Supported Models
Cline supports the following Mistral models:
- pixtral-large-2411
- ministral-3b-2410
- ministral-8b-2410
- mistral-small-latest
- mistral-medium-latest
- mistral-small-2501
- pixtral-12b-2409
- open-mistral-nemo-2407
- open-codestral-mamba
- codestral-2501
- devstral-small-2505
**Note:** Model availability and specifications may change.
Refer to the [Mistral AI documentation](https://docs.mistral.ai/api/) and [Mistral Model Overview](https://docs.mistral.ai/getting-started/models/models_overview/) for the most current information.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Mistral" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Mistral API key into the "Mistral API Key" field if you're using a standard `mistral` model. If you intend to use `codestral-latest`, see the "Using Codestral" section below.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
### Using Codestral
[Codestral](https://docs.mistral.ai/capabilities/code_generation/) is a model specifically designed for code generation and interaction.
For Codestral, you can use different endpoints (Default: codestral.mistral.ai).
If using the La Plateforme API Key for Codestral, change the **Codestral Base Url** to: `https://api.mistral.ai`
To use Codestral with Cline:
1. **Select "Mistral" as the API Provider in Cline Settings.**
2. **Select a Codestral Model** (e.g., `codestral-latest`) from the "Model" dropdown.
3. **Enter your Codestral API Key** (from `codestral.mistral.ai`) or your La Plateforme API Key (from `api.mistral.ai`) into the appropriate API key field in Cline.
+78
View File
@@ -0,0 +1,78 @@
---
title: "Ollama"
---
Cline supports running models locally using Ollama. This approach offers privacy, offline access, and potentially reduced costs. It requires some initial setup and a sufficiently powerful computer. Because of the present state of consumer hardware, it's not recommended to use Ollama with Cline as performance will likely be poor for average hardware configurations.
**Website:** [https://ollama.com/](https://ollama.com/)
### Setting up Ollama
1. **Download and Install Ollama:**
Obtain the Ollama installer for your operating system from the [Ollama website](https://ollama.com/) and follow their installation guide. Ensure Ollama is running. You can typically start it with:
```bash
ollama serve
```
2. **Download a Model:**
Ollama supports a wide variety of models. A list of available models can be found on the [Ollama model library](https://ollama.com/library). Some models recommended for coding tasks include:
- `codellama:7b-code` (a good, smaller starting point)
- `codellama:13b-code` (offers better quality, larger size)
- `codellama:34b-code` (provides even higher quality, very large)
- `qwen2.5-coder:32b`
- `mistralai/Mistral-7B-Instruct-v0.1` (a solid general-purpose model)
- `deepseek-coder:6.7b-base` (effective for coding)
- `llama3:8b-instruct-q5_1` (suitable for general tasks)
To download a model, open your terminal and execute:
```bash
ollama pull <model_name>
```
For instance:
```bash
ollama pull qwen2.5-coder:32b
```
3. **Configure the Model's Context Window:**
By default, Ollama models often use a context window of 2048 tokens, which can be insufficient for many Cline requests. A minimum of 12,000 tokens is advisable for decent results, with 32,000 tokens being ideal. To adjust this, you'll modify the model's parameters and save it as a new version.
First, load the model (using `qwen2.5-coder:32b` as an example):
```bash
ollama run qwen2.5-coder:32b
```
Once the model is loaded within the Ollama interactive session, set the context size parameter:
```
/set parameter num_ctx 32768
```
Then, save this configured model with a new name:
```
/save your_custom_model_name
```
(Replace `your_custom_model_name` with a name of your choice.)
4. **Configure Cline:**
- Open the Cline sidebar (usually indicated by the Cline icon).
- Click the settings gear icon (⚙️).
- Select "ollama" as the API Provider.
- Enter the Model name you saved in the previous step (e.g., `your_custom_model_name`).
- (Optional) Adjust the base URL if Ollama is running on a different machine or port. The default is `http://localhost:11434`.
- (Optional) Configure the Model context size in Cline's Advanced settings. This helps Cline manage its context window effectively with your customized Ollama model.
### Tips and Notes
- **Resource Demands:** Running large language models locally can be demanding on system resources. Ensure your computer meets the requirements for your chosen model.
- **Model Choice:** Experiment with various models to discover which best fits your specific tasks and preferences.
- **Offline Capability:** After downloading a model, you can use Cline with that model even without an internet connection.
- **Token Usage Tracking:** Cline tracks token usage for models accessed via Ollama, allowing you to monitor consumption.
- **Ollama's Own Documentation:** For more detailed information, consult the official [Ollama documentation](https://ollama.com/docs).
@@ -0,0 +1,72 @@
---
title: "OpenAI Compatible"
description: "Learn how to configure Cline with various AI model providers that offer OpenAI-compatible APIs."
---
Cline supports a wide range of AI model providers that offer APIs compatible with the OpenAI API standard. This allows you to use models from providers _other than_ OpenAI, while still utilizing a familiar API interface. This includes providers such as:
- **Local models** running through tools like Ollama and LM Studio (which are covered in their respective sections).
- **Cloud providers** like Perplexity, Together AI, Anyscale, and many others.
- **Any other provider** that offers an OpenAI-compatible API endpoint.
This document focuses on setting up providers _other than_ the official OpenAI API (which has its own [dedicated configuration page](/provider-config/openai)).
### General Configuration
The key to using an OpenAI-compatible provider with Cline is to configure these main settings:
1. **Base URL:** This is the API endpoint specific to the provider. It will _not_ be `https://api.openai.com/v1` (that URL is for the official OpenAI API).
2. **API Key:** This is the secret key you obtain from your chosen provider.
3. **Model ID:** This is the specific name or identifier for the model you wish to use.
You'll find these settings in the Cline settings panel (click the ⚙️ icon):
- **API Provider:** Select "OpenAI Compatible".
- **Base URL:** Enter the base URL provided by your chosen provider. **This is a crucial step.**
- **API Key:** Enter your API key from the provider.
- **Model:** Choose or enter the model ID.
- **Model Configuration:** This section allows you to customize advanced parameters for the model, such as:
- Max Output Tokens
- Context Window size
- Image Support capabilities
- Computer Use (e.g., for models with tool/function calling)
- Input Price (per token/million tokens)
- Output Price (per token/million tokens)
### Supported Models (for OpenAI Native Endpoint)
While the "OpenAI Compatible" provider type allows connecting to various endpoints, if you are connecting directly to the official OpenAI API (or an endpoint that mirrors it exactly), Cline recognizes the following model IDs based on the `openAiNativeModels` definition in its source code:
- `o3-mini`
- `o3-mini-high`
- `o3-mini-low`
- `o1`
- `o1-preview`
- `o1-mini`
- `gpt-4.5-preview`
- `gpt-4o`
- `gpt-4o-mini`
**Note:** If you are using a different OpenAI-compatible provider (such as Together AI, Anyscale, etc.), the available model IDs will differ. Always refer to your specific provider's documentation for their supported model names and any unique configuration details.
### v0 (Vercel SDK) in Cline:
- For developers working with v0, their [AI SDK documentation](https://vercel.com/docs/v0/cline) provides valuable insights and examples for integrating various models, many of which are OpenAI-compatible. This can be a helpful resource for understanding how to structure calls and manage configurations when using Cline with services deployed on or integrated with Vercel.
- v0 can be used in Cline with the OpenAI Compatible provider.
- ### Quickstart
- 1. With the OpenAI Compatible provider selected, set the Base URL to https://api.v0.dev/v1.
- 2. Paste in your v0 API Key
- 3. Set the Model ID: v0-1.0-md
- 4. Click Verify to confirm the connection.
### Troubleshooting
- **"Invalid API Key":** Double-check that you've entered the API key correctly and that it's for the correct provider.
- **"Model Not Found":** Ensure you're using a valid model ID for your chosen provider and that it's available at the specified Base URL.
- **Connection Errors:** Verify the Base URL is correct, that your provider's API is accessible from your machine, and that there are no firewall or network issues.
- **Unexpected Results:** If you're getting unexpected outputs, try a different model or double-check all configuration parameters.
By using an OpenAI-compatible provider, you can leverage the flexibility of Cline with a wider array of AI models. Remember to always consult your provider's documentation for the most accurate and up-to-date information.
+48
View File
@@ -0,0 +1,48 @@
---
title: "OpenAI"
description: "Learn how to configure and use official OpenAI models with Cline."
---
Cline supports accessing models directly through the official OpenAI API.
**Website:** [https://openai.com/](https://openai.com/)
### Getting an API Key
1. **Sign Up/Sign In:** Visit the [OpenAI Platform](https://platform.openai.com/). You'll need to create an account or sign in if you already have one.
2. **Navigate to API Keys:** Once logged in, go to the [API keys section](https://platform.openai.com/api-keys) of your account.
3. **Create a Key:** Click on "Create new secret key". It's good practice to give your key a descriptive name (e.g., "Cline API Key").
4. **Copy the Key:** **Crucial:** Copy the generated API key immediately. For security reasons, OpenAI will not show it to you again. Store this key in a safe and secure location.
### Supported Models
Cline is compatible with a variety of OpenAI models, including but not limited to:
- 'o3'
- `o3-mini` (medium reasoning effort)
- 'o4-mini'
- `o3-mini-high` (high reasoning effort)
- `o3-mini-low` (low reasoning effort)
- `o1`
- `o1-preview`
- `o1-mini`
- `gpt-4.5-preview`
- `gpt-4o`
- `gpt-4o-mini`
- 'gpt-4.1'
- 'gpt-4.1-mini'
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
### Configuration in Cline
1. **Open Cline Settings:** Click the settings gear icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "OpenAI" from the "API Provider" dropdown menu.
3. **Enter API Key:** Paste your OpenAI API key into the "OpenAI API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown list.
5. **(Optional) Base URL:** If you need to use a proxy or a custom base URL for the OpenAI API, you can enter it here. Most users will not need to change this from the default.
### Tips and Notes
- **Pricing:** Be sure to review the [OpenAI Pricing page](https://openai.com/pricing) for detailed information on the costs associated with different models.
- **Azure OpenAI Service:** If you are looking to use the Azure OpenAI service, please note that specific documentation for Azure OpenAI with Cline may be found separately, or you might need to configure it as an OpenAI-compatible endpoint if such functionality is supported by Cline for custom configurations.
+40
View File
@@ -0,0 +1,40 @@
---
title: "OpenRouter"
description: "Learn how to use OpenRouter with Cline to access a wide variety of language models through a single API."
---
OpenRouter is an AI platform that provides access to a wide variety of language models from different providers, all through a single API. This can simplify setup and allow you to easily experiment with different models.
**Website:** [https://openrouter.ai/](https://openrouter.ai/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [OpenRouter website](https://openrouter.ai/). Sign in with your Google or GitHub account.
2. **Get an API Key:** Go to the [keys page](https://openrouter.ai/keys). You should see an API key listed. If not, create a new key.
3. **Copy the Key:** Copy the API key.
### Supported Models
OpenRouter supports a large and growing number of models. Cline automatically fetches the list of available models. Refer to the [OpenRouter Models page](https://openrouter.ai/models) for the complete and up-to-date list.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "OpenRouter" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your OpenRouter API key into the "OpenRouter API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
5. **(Optional) Custom Base URL:** If you need to use a custom base URL for the OpenRouter API, check "Use custom base URL" and enter the URL. Leave this blank for most users.
### Supported Transforms
OpenRouter provides an [optional "middle-out" message transform](https://openrouter.ai/docs/features/message-transforms) to help with prompts that exceed the maximum context size of a model. You can enable it by checking the "Compress prompts and message chains to the context size" box.
### Tips and Notes
- **Model Selection:** OpenRouter offers a wide range of models. Experiment to find the best one for your needs.
- **Pricing:** OpenRouter charges based on the underlying model's pricing. See the [OpenRouter Models page](https://openrouter.ai/models) for details.
- **Prompt Caching:**
- OpenRouter passes caching requests to underlying models that support it. Check the [OpenRouter Models page](https://openrouter.ai/models) to see which models offer caching.
- For most models, caching should activate automatically if supported by the model itself (similar to how Requesty works).
- **Exception for Gemini Models via OpenRouter:** Due to potential response delays sometimes observed with Google's caching mechanism when accessed via OpenRouter, a manual activation step is required _specifically for Gemini models_.
- If using a **Gemini model** via OpenRouter, you **must manually check** the "Enable Prompt Caching" box in the provider settings to activate caching for that model. This checkbox serves as a temporary workaround. For non-Gemini models on OpenRouter, this checkbox is not necessary for caching.
+38
View File
@@ -0,0 +1,38 @@
---
title: "Requesty"
description: "Learn how to use Requesty with Cline to access and optimize over 150 large language models."
---
Cline supports accessing models through the [Requesty](https://www.requesty.ai/) AI platform. Requesty provides an easy and optimized API for interacting with 150+ large language models (LLMs).
**Website:** [https://www.requesty.ai/](https://www.requesty.ai/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [Requesty website](https://www.requesty.ai/) and create an account or sign in.
2. **Get API Key:** You can get an API key from the [API Management](https://app.requesty.ai/manage-api) section of your Requesty dashboard.
### Supported Models
Requesty provides access to a wide range of models. Cline will automatically fetch the latest list of available models. You can see the full list of available models on the [Model List](https://app.requesty.ai/router/list) page.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Requesty" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Requesty API key into the "Requesty API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
### Tips and Notes
- **Optimizations**: Requesty offers a range of in-flight cost optimizations to lower your costs.
- **Unified and simplified billing**: Unrestricted access to all providers and models, automatic balance top ups and more via a single [API key](https://app.requesty.ai/manage-api).
- **Cost tracking**: Track cost per model, coding language, changed file, and more via the [Cost dashboard](https://app.requesty.ai/cost-management) or the [Requesty VS Code extension](https://marketplace.visualstudio.com/items?itemName=Requesty.requesty).
- **Stats and logs**: See your [coding stats dashboard](https://app.requesty.ai/usage-stats) or go through your [LLM interaction logs](https://app.requesty.ai/logs).
- **Fallback policies**: Keep your LLM working for you with fallback policies when providers are down.
- **Prompt Caching:** Some providers support prompt caching. [Search models with caching](https://app.requesty.ai/router/list).
### Relevant resources
- [Requesty Youtube channel](https://www.youtube.com/@requestyAI)
- [Requesty Discord](https://requesty.ai/discord)
@@ -15,17 +15,24 @@ Cline offers _experimental_ support for the [VS Code Language Model API](https:/
- **VS Code:** The Language Model API is accessible via VS Code (it is not currently supported by Cursor).
- **A Language Model Provider Extension:** An extension that furnishes a language model is required. Examples include:
- **GitHub Copilot:** With a Copilot subscription, the GitHub Copilot and GitHub Copilot Chat extensions can serve as model providers.
- **Alternative Extensions:** Explore the VS Code Marketplace for extensions mentioning "Language Model API" or "lm". Other experimental options may be available.
- **Alternative Extensions:** Explore the VS Code Marketplace for extensions mentioning "Language Model API" or "lm". Other experimental options may be available
### Configuration Steps
1. **Access Cline Settings:** Click the gear icon (⚙️) located in the Cline panel.
2. **Choose Provider:** Select "VS Code LM API" from the "API Provider" dropdown menu.
3. **Select Model:** The "Language Model" dropdown will (eventually) populate with available models. The naming convention is `vendor/family`. For instance, if Copilot is active, you might encounter options such as:
- `copilot - claude-3.5-sonnet`
- `copilot - o3-mini`
- `copilot - o1-ga`
- `copilot - gemini-2.0-flash`
1. **Ensure Copilot Account is Active and Extensions are installed:** User logged into either the Copilot or Copilot Chat extension should be able to gain access via Cline.
2. **Access Cline Settings:** Click the gear icon (⚙️) located in the Cline panel.
3. **Choose Provider:** Select "VS Code LM API" from the "API Provider" dropdown menu.
4. **Select Model:** If the Copilot extension(s) are installed and the user is logged into their Copilot account, the "Language Model" dropdown will populate with available models after a short time. The naming convention is `vendor/family`. For instance, if Copilot is active, you might encounter options such as:
- `copilot - gpt-3.5-turbo`
- `copilot - gpt-4o-mini`
- `copilot - gpt-4`
- `copilot - gpt-4-turbo`
- `copilot - gpt-4o`
- `copilot - claude-3.5-sonnet` **NOTE:** this model does not work.
- `copilot - gemini-2.0-flash`
- `copilot - gpt-4.1`
For best results with the VSCode LM API Provider, we suggest using the OpenAI Models (GPT 3, 4, 4.1, 4o etc.)
### Current Limitations
+85
View File
@@ -0,0 +1,85 @@
---
title: "xAI (Grok)"
description: "Learn how to configure and use xAI's Grok models with Cline, including API key setup, supported models, and reasoning capabilities."
---
xAI is the company behind Grok, a large language model known for its conversational abilities and large context window. Grok models are designed to provide helpful, informative, and contextually relevant responses.
**Website:** [https://x.ai/](https://x.ai/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to the [xAI Console](https://console.x.ai/). Create an account or sign in.
2. **Navigate to API Keys:** Go to the API keys section in your dashboard.
3. **Create a Key:** Click to create a new API key. Give your key a descriptive name (e.g., "Cline").
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
### Supported Models
Cline supports the following xAI Grok models:
#### Grok-3 Models
- `grok-3-beta` (Default) - xAI's Grok-3 beta model with 131K context window
- `grok-3-fast-beta` - xAI's Grok-3 fast beta model with 131K context window
- `grok-3-mini-beta` - xAI's Grok-3 mini beta model with 131K context window
- `grok-3-mini-fast-beta` - xAI's Grok-3 mini fast beta model with 131K context window
#### Grok-2 Models
- `grok-2-latest` - xAI's Grok-2 model - latest version with 131K context window
- `grok-2` - xAI's Grok-2 model with 131K context window
- `grok-2-1212` - xAI's Grok-2 model (version 1212) with 131K context window
#### Grok Vision Models
- `grok-2-vision-latest` - xAI's Grok-2 Vision model - latest version with image support and 32K context window
- `grok-2-vision` - xAI's Grok-2 Vision model with image support and 32K context window
- `grok-2-vision-1212` - xAI's Grok-2 Vision model (version 1212) with image support and 32K context window
- `grok-vision-beta` - xAI's Grok Vision Beta model with image support and 8K context window
#### Legacy Models
- `grok-beta` - xAI's Grok Beta model (legacy) with 131K context window
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "xAI" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your xAI API key into the "xAI API Key" field.
4. **Select Model:** Choose your desired Grok model from the "Model" dropdown.
### Reasoning Capabilities
Grok 3 Mini models feature specialized reasoning capabilities, allowing them to "think before responding" - particularly useful for complex problem-solving tasks.
#### Reasoning-Enabled Models
Reasoning is only supported by:
- `grok-3-mini-beta`
- `grok-3-mini-fast-beta`
The Grok 3 models `grok-3-beta` and `grok-3-fast-beta` do not support reasoning.
#### Controlling Reasoning Effort
When using reasoning-enabled models, you can control how hard the model thinks with the `reasoning_effort` parameter:
- `low`: Minimal thinking time, using fewer tokens for quick responses
- `high`: Maximum thinking time, leveraging more tokens for complex problems
Choose `low` for simple queries that should complete quickly, and `high` for harder problems where response latency is less important.
#### Key Features
- **Step-by-Step Problem Solving**: The model thinks through problems methodically before delivering an answer
- **Math & Quantitative Strength**: Excels at numerical challenges and logic puzzles
- **Reasoning Trace Access**: The model's thinking process is available via the `reasoning_content` field in the response completion object
### Tips and Notes
- **Context Window:** Most Grok models feature large context windows (up to 131K tokens), allowing you to include substantial amounts of code and context in your prompts.
- **Vision Capabilities:** Select vision-enabled models (`grok-2-vision-latest`, `grok-2-vision`, etc.) when you need to process or analyze images.
- **Pricing:** Pricing varies by model, with input costs ranging from $0.3 to $5.0 per million tokens and output costs from $0.5 to $25.0 per million tokens. Refer to the xAI documentation for the most current pricing information.
- **Performance Tradeoffs:** "Fast" variants typically offer quicker response times but may have higher costs, while "mini" variants are more economical but may have reduced capabilities.
@@ -0,0 +1,214 @@
const { RuleTester } = require("eslint")
const rule = require("../no-protobuf-object-literals")
const ruleTester = new RuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
ruleTester.run("no-protobuf-object-literals", rule, {
valid: [
// Valid case: Using .create() method
{
code: `
import { State } from '@shared/proto/state';
const state = State.create({
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
});
`,
},
// Valid case: Using .fromPartial() method
{
code: `
import { ChatSettings } from '@shared/proto/state';
const settings = ChatSettings.fromPartial({
mode: 0,
preferredLanguage: 'en',
openAiReasoningEffort: 'thorough'
});
`,
},
// Valid case: Object literal not used with protobuf type
{
code: `
interface MyInterface {
id: number;
name: string;
}
const obj: MyInterface = {
id: 123,
name: 'test'
};
`,
},
// Valid case: Using object literal for non-protobuf import
{
code: `
import { SomeType } from '@some/other/package';
const obj: SomeType = {
id: 123,
name: 'test'
};
`,
},
// Valid case: Regular function call with object literal (should not be flagged)
{
code: `
import { State } from '@shared/proto/state';
// This should not be flagged because it's a regular function call
// not directly tied to a protobuf type
process({
id: 123,
name: 'test',
data: { nested: true }
});
`,
},
],
invalid: [
// Invalid case: Using object literal with imported protobuf type
{
code: `
import { State } from '@shared/proto/state';
const state: State = {
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
};
`,
output: `
import { State } from '@shared/proto/state';
const state: State = State.create({
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
});
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Invalid case: Using object literal with namespaced protobuf type
{
code: `
import * as stateProto from '@shared/proto/state';
const state: stateProto.State = {
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
};
`,
output: `
import * as stateProto from '@shared/proto/state';
const state: stateProto.State = stateProto.State.create({
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
});
`,
errors: [{ messageId: "useProtobufMethodGeneric" }],
},
// Invalid case: Using object literal in a return statement (with protobuf return type)
{
code: `
import { ChatSettings } from '@shared/proto/state';
function createSettings(): ChatSettings {
return {
mode: 0,
preferredLanguage: 'en',
openAiReasoningEffort: 'thorough'
};
}
`,
output: `
import { ChatSettings } from '@shared/proto/state';
function createSettings(): ChatSettings {
return ChatSettings.create({
mode: 0,
preferredLanguage: 'en',
openAiReasoningEffort: 'thorough'
});
}
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Invalid case: Using object literal in a function parameter (with protobuf types imported)
{
code: `
import { ChatContent } from '@shared/proto/state';
function processContent(content: ChatContent) {
// process the content
}
processContent({
message: 'Hello, this is a test message',
images: ['image1.png', 'image2.jpg'],
files: ['file1.txt', 'file2.pdf']
});
`,
output: `
import { ChatContent } from '@shared/proto/state';
function processContent(content: ChatContent) {
// process the content
}
processContent(ChatContent.create({
message: 'Hello, this is a test message',
images: ['image1.png', 'image2.jpg'],
files: ['file1.txt', 'file2.pdf']
}));
`,
errors: [{ messageId: "useProtobufMethodGeneric" }],
},
// Invalid case: Using object literal in assignment expression
{
code: `
import { State } from '@shared/proto/state';
let state: State;
state = {
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
};
`,
output: `
import { State } from '@shared/proto/state';
let state: State;
state = State.create({
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
});
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Test with custom protobufPackages option
{
code: `
import { CustomProto } from 'custom/proto/package';
const obj: CustomProto = {
field1: 'value',
field2: 123
};
`,
output: `
import { CustomProto } from 'custom/proto/package';
const obj: CustomProto = CustomProto.create({
field1: 'value',
field2: 123
});
`,
options: [{ protobufPackages: ["custom/proto"] }],
errors: [{ messageId: "useProtobufMethod" }],
},
],
})
+3
View File
@@ -1,14 +1,17 @@
// eslint-rules/index.js
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
module.exports = {
rules: {
"no-protobuf-object-literals": noProtobufObjectLiterals,
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
},
configs: {
recommended: {
plugins: ["local"],
rules: {
"local/no-protobuf-object-literals": "error",
"local/no-grpc-client-object-literals": "error",
},
},
+556
View File
@@ -0,0 +1,556 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
module.exports = createRule({
name: "no-protobuf-object-literals",
meta: {
type: "problem",
docs: {
description: "Enforce using .create() or .fromPartial() for protobuf objects instead of object literals",
recommended: "error",
},
fixable: "code",
messages: {
useProtobufMethod:
"Use {{typeName}}.create() or {{typeName}}.fromPartial() instead of " +
"object literal for protobuf type from @shared/proto\n" +
"Found: {{code}}\n Suggestion: " +
"{{typeName}}.create({{objectContent}})",
useProtobufMethodGeneric:
"Use .create() or .fromPartial() instead of object literal for protobuf " +
"type from @shared/proto\n Found: {{code}}",
},
schema: [
{
type: "object",
properties: {
protobufPackages: {
type: "array",
items: { type: "string" },
default: ["shared/proto/"],
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ protobufPackages: ["shared/proto/"] }],
create(context, [options]) {
const protobufPackages = options.protobufPackages
const protobufImports = new Set() // Set of imported protobuf types
const protobufNamespaceImports = new Set() // For namespace imports like "import * as proto"
const safeObjectExpressions = new Set() // Track object expressions in create/fromPartial calls
return {
// Skip object literals inside create() or fromPartial() method calls
CallExpression(node) {
if (
node.callee &&
node.callee.type === "MemberExpression" &&
(node.callee.property.name === "create" || node.callee.property.name === "fromPartial") &&
node.arguments.length > 0 &&
node.arguments[0].type === "ObjectExpression"
) {
// Track this object expression as being used with create/fromPartial
safeObjectExpressions.add(node.arguments[0])
}
},
// Track imports from protobuf packages
ImportDeclaration(node) {
const packageName = node.source.value
if (matchesProtobufPackage(packageName, protobufPackages)) {
// This is a protobuf package.
node.specifiers.forEach((spec) => {
if (spec.type === "ImportSpecifier") {
// import { MyRequest } from '@shared/proto'
protobufImports.add(spec.imported.name)
} else if (spec.type === "ImportNamespaceSpecifier") {
// import * as proto from '@shared/proto'
protobufNamespaceImports.add(spec.local.name)
}
})
}
},
// Check variable declarations with type annotations
"VariableDeclarator > ObjectExpression"(node) {
// Skip if this is inside a create/fromPartial call
if (safeObjectExpressions.has(node)) {
return
}
// Found object literal in variable declaration
const declarator = node.parent
if (declarator.id && declarator.id.typeAnnotation) {
const typeName = getTypeName(declarator.id.typeAnnotation.typeAnnotation)
if (typeName) {
// Check if it's a direct protobuf import
if (protobufImports.has(typeName)) {
//console.log('🚨 VIOLATION: Using object literal for protobuf type:', typeName);
const sourceCode = context.getSourceCode()
const declaratorText = sourceCode.getText(declarator)
const objectText = sourceCode.getText(node)
context.report({
node,
messageId: "useProtobufMethod",
data: {
typeName,
code: declaratorText,
objectContent: objectText,
},
fix(fixer) {
// Replace the object literal with Type.create() call
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
},
})
return
}
// Check if it's a namespaced protobuf type (e.g., proto.MyRequest)
if (isNamespacedProtobufType(protobufNamespaceImports, typeName)) {
//console.log('🚨 VIOLATION: Using object literal for namespaced protobuf type:', typeName);
const sourceCode = context.getSourceCode()
const declaratorText = sourceCode.getText(declarator)
context.report({
node,
messageId: "useProtobufMethodGeneric",
data: { code: declaratorText },
fix(fixer) {
// For namespaced types, use the full type name to call create()
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
},
})
}
}
}
},
// Check assignment expressions
"AssignmentExpression > ObjectExpression"(node) {
// Skip if this is inside a create/fromPartial call
if (safeObjectExpressions.has(node)) {
return
}
const assignment = node.parent
// For assignment to variables without inline type annotation
if (assignment.left && assignment.right === node) {
let typeName = null
// Check if there's a typeAnnotation directly on the left
if (assignment.left.typeAnnotation) {
typeName = getTypeName(assignment.left.typeAnnotation.typeAnnotation)
}
// Otherwise try to infer from the variable name if it's a simple identifier
else if (assignment.left.type === "Identifier") {
const varName = assignment.left.name
// Check variable declarations in the current scope
const sourceCode = context.getSourceCode()
const scope = sourceCode.getScope(node)
const variable = scope.variables.find((v) => v.name === varName)
if (variable && variable.defs.length > 0) {
const def = variable.defs[0]
if (def.node.id && def.node.id.typeAnnotation) {
typeName = getTypeName(def.node.id.typeAnnotation.typeAnnotation)
}
}
}
if (typeName && protobufImports.has(typeName)) {
//console.log('🚨 VIOLATION: Using object literal in assignment for protobuf type:', typeName);
const sourceCode = context.getSourceCode()
const assignmentText = sourceCode.getText(assignment.left) + " = "
const objectText = sourceCode.getText(node)
context.report({
node,
messageId: "useProtobufMethod",
data: {
typeName,
code: assignmentText + "{",
objectContent: objectText,
},
fix(fixer) {
// Replace the object literal with Type.create() call in assignments
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
},
})
}
}
},
// Check return statements
"ReturnStatement > ObjectExpression"(node) {
// Skip if this is inside a create/fromPartial call
if (safeObjectExpressions.has(node)) {
return
}
// Find the parent function to get its return type
const functionNode = findParentFunction(node)
if (!functionNode) {
return
}
// Try to get the return type using our enhanced helper
const sourceCode = context.getSourceCode()
let returnTypeName = getFunctionReturnType(functionNode, sourceCode)
// For async functions with Promise<Type> return type, extract the inner type
if (returnTypeName && returnTypeName.startsWith("Promise<") && returnTypeName.endsWith(">")) {
returnTypeName = returnTypeName.slice(8, -1)
}
// Check if the return type is a protobuf type
if (returnTypeName) {
if (protobufImports.has(returnTypeName)) {
//console.log('🚨 VIOLATION: Return type is a protobuf type:', returnTypeName);
const sourceCode = context.getSourceCode()
const returnText = sourceCode.getText(node.parent)
context.report({
node,
messageId: "useProtobufMethod",
data: {
typeName: returnTypeName,
code: returnText,
objectContent: sourceCode.getText(node),
},
fix(fixer) {
// Replace the object literal with Type.create() call in return statements
return fixer.replaceText(node, `${returnTypeName}.create(${sourceCode.getText(node)})`)
},
})
return
}
// Check if it's a namespaced protobuf type
if (isNamespacedProtobufType(protobufNamespaceImports, returnTypeName)) {
const sourceCode = context.getSourceCode()
const returnText = sourceCode.getText(node.parent)
//console.log('🚨 VIOLATION: Return type is a namespaced protobuf type:', returnTypeName);
context.report({
node,
messageId: "useProtobufMethodGeneric",
data: { code: returnText },
fix(fixer) {
// For namespaced types in return statements, we need to extract the full type name
const objectCode = sourceCode.getText(node)
// Since we may not know the exact type, we'll use the more generic namespaced type
return fixer.replaceText(node, `${returnTypeName}.create(${objectCode})`)
},
})
return
}
}
// Final fallback - if there are any protobuf imports and the function signature
// mentions a return type that matches one of the imported types
const functionText = functionNode ? sourceCode.getText(functionNode) : ""
for (const protoType of protobufImports) {
// Use more precise regex to match return type patterns specifically
// Rather than just checking if the type name appears anywhere in the signature
const returnTypeRegex = new RegExp(
// Match arrow function return type
`=>\\s*:?\\s*${protoType}\\b|` +
// Match function declaration return type
`\\)\\s*:?\\s*${protoType}\\b|` +
// Match Promise return type
`\\)\\s*:?\\s*Promise<\\s*${protoType}\\s*>|` +
// Match function type in variable declaration
`:\\s*\\(.*\\)\\s*=>\\s*${protoType}\\b`,
)
if (returnTypeRegex.test(functionText)) {
const returnText = sourceCode.getText(node.parent)
//console.log('🚨 VIOLATION: regex matched protobuf type:', functionText);
context.report({
node,
messageId: "useProtobufMethod",
data: {
typeName: protoType,
code: returnText,
objectContent: sourceCode.getText(node),
},
fix(fixer) {
// Replace the object literal with Type.create() call
return fixer.replaceText(node, `${protoType}.create(${sourceCode.getText(node)})`)
},
})
return
}
}
// Check for namespace imports too
for (const namespace of protobufNamespaceImports) {
// Similar to above, but for namespaced types
const namespaceReturnTypeRegex = new RegExp(
// Match arrow function return type
`=>\\s*:?\\s*${namespace}\\.\\w+\\b|` +
// Match function declaration return type
`\\)\\s*:?\\s*${namespace}\\.\\w+\\b|` +
// Match Promise return type
`\\)\\s*:?\\s*Promise<\\s*${namespace}\\.\\w+\\s*>|` +
// Match function type in variable declaration
`:\\s*\\(.*\\)\\s*=>\\s*${namespace}\\.\\w+\\b`,
)
if (namespaceReturnTypeRegex.test(functionText)) {
const returnText = sourceCode.getText(node.parent)
//console.log('🚨 VIOLATION: regex matched namespaced protobuf type:', functionText, "namespace:", namespace);
context.report({
node,
messageId: "useProtobufMethodGeneric",
data: { code: returnText },
fix(fixer) {
// For namespaced types based on function signature patterns
// Extract the namespace and type from the function text using more precise patterns
const match = functionText.match(
new RegExp(
// Match return type patterns more precisely
`\\)\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Function declaration
`=>\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Arrow function
`Promise<\\s*(${namespace}\\.[\\w]+)\\s*>`, // Promise wrapped
),
)
if (match) {
const fullType = match[1] || match[2]
return fixer.replaceText(node, `${fullType}.create(${sourceCode.getText(node)})`)
}
// Fallback - we can't determine the exact type, but we know it's from the namespace
// Use a namespace-based approach
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
},
})
return
}
}
},
// Check function call arguments (more selective approach)
"CallExpression > ObjectExpression"(node) {
// Skip if this is inside a create/fromPartial call
if (safeObjectExpressions.has(node)) {
return
}
// We need to be more selective to avoid false positives
// Only warn if:
// 1. The function is called on a protobuf namespace
// 2. The call argument has a type annotation that matches a protobuf type
// 3. The call is to a function that we know takes a protobuf type
// Check if it's a call on a protobuf namespace
if (
node.parent.callee &&
node.parent.callee.type === "MemberExpression" &&
node.parent.callee.object.type === "Identifier"
) {
const namespace = node.parent.callee.object.name
if (protobufNamespaceImports.has(namespace)) {
const sourceCode = context.getSourceCode()
const callText = sourceCode.getText(node.parent)
//console.log('🚨 VIOLATION: Check function call arguments object literal:', callText);
context.report({
node,
messageId: "useProtobufMethodGeneric",
data: { code: callText },
fix(fixer) {
// For calls on a protobuf namespace
const memberExpr = node.parent.callee
// Try to determine if this is calling a method that expects a specific type
const methodName = memberExpr.property.name
// If method name looks like 'create' + Type, we can infer the type
const possibleTypeName = methodName.replace(/^create/, "")
// Check if namespace has a type with this name
// Since we can't directly check at lint time, we'll use the namespace + inferred type
if (possibleTypeName && possibleTypeName !== methodName) {
return fixer.replaceText(
node,
`${namespace}.${possibleTypeName}.create(${sourceCode.getText(node)})`,
)
}
// Fallback - use a more generic approach with namespace
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
},
})
return
}
}
// For regular function calls with object literals, check if there are protobuf imports
// and if the function might expect a protobuf type
if (node.parent.callee) {
// This is a more permissive check to catch cases like processContent({ ... })
// which might be passing a protobuf type
const sourceCode = context.getSourceCode()
const scope = sourceCode.getScope(node)
// Try to find the function definition
if (node.parent.callee.type === "Identifier") {
const functionName = node.parent.callee.name
const variable = scope.variables.find((v) => v.name === functionName)
// If we found the function and it has parameter type annotations
// that match protobuf types, flag it
if (variable && variable.defs.length > 0) {
const def = variable.defs[0]
if (def.node.params && node.parent.arguments.indexOf(node) < def.node.params.length) {
const param = def.node.params[node.parent.arguments.indexOf(node)]
if (param.typeAnnotation) {
const typeName = getTypeName(param.typeAnnotation.typeAnnotation)
if (
typeName &&
(protobufImports.has(typeName) ||
isNamespacedProtobufType(protobufNamespaceImports, typeName))
) {
const callText = sourceCode.getText(node.parent)
//console.log('🚨 VIOLATION: Function call arguments object literal:', callText);
context.report({
node,
messageId: "useProtobufMethodGeneric",
data: { code: callText },
fix(fixer) {
// For function calls with protobuf type parameters
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
},
})
return
}
}
}
}
}
}
},
}
},
})
// Helper functions
function getTypeName(typeAnnotation) {
if (!typeAnnotation) {
return null
}
if (typeAnnotation.type === "TSTypeReference") {
if (typeAnnotation.typeName.type === "Identifier") {
return typeAnnotation.typeName.name
} else if (typeAnnotation.typeName.type === "TSQualifiedName") {
// Handle namespaced types like proto.MyRequest
return `${typeAnnotation.typeName.left.name}.${typeAnnotation.typeName.right.name}`
}
}
return null
}
function matchesProtobufPackage(packageName, protobufPackages) {
return protobufPackages.some((protobufPackage) => {
// Remove leading and trailing @ and / from protobufPackage
const cleanedPackage = protobufPackage.replace(/^[@\/]/, "").replace(/[\/]$/, "")
const pattern = new RegExp(`(.*[@/]|)${escapeRegex(cleanedPackage)}[/].*`)
return pattern.test(packageName)
})
}
// Helper function to escape special regex characters
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
// Helper to extract function return type more reliably
function getFunctionReturnType(functionNode, sourceCode) {
// 1. Check explicit return type annotation
if (functionNode.returnType) {
return getTypeName(functionNode.returnType.typeAnnotation)
}
// 2. For variable declarations like const foo: (arg: Type) => ReturnType = ...
if (functionNode.parent && functionNode.parent.type === "VariableDeclarator") {
const declarator = functionNode.parent
if (declarator.id && declarator.id.typeAnnotation) {
const typeAnnotation = declarator.id.typeAnnotation.typeAnnotation
// Handle function type annotations
if (typeAnnotation.type === "TSFunctionType" && typeAnnotation.typeAnnotation) {
return getTypeName(typeAnnotation.typeAnnotation)
}
// Handle type references to function types
if (typeAnnotation.type === "TSTypeReference") {
// This might be a type like Promise<ReturnType>
if (
typeAnnotation.typeName.name === "Promise" &&
typeAnnotation.typeParameters &&
typeAnnotation.typeParameters.params.length > 0
) {
return getTypeName(typeAnnotation.typeParameters.params[0])
}
}
}
}
// 3. For class methods, check if it's part of an interface implementation
if (
functionNode.parent &&
functionNode.parent.type === "MethodDefinition" &&
functionNode.parent.parent &&
functionNode.parent.parent.type === "ClassBody"
) {
const className = getEnclosingClassName(functionNode)
const methodName = functionNode.parent.key.name
if (className && methodName) {
// Look for interface declarations in the scope
const scope = sourceCode.getScope(functionNode)
// This would require more complex scope analysis which is limited in ESLint
// For now, we'll return null and rely on other methods
}
}
return null
}
// Helper to get the class name for a method
function getEnclosingClassName(node) {
let current = node.parent
while (current) {
if (current.type === "ClassDeclaration" && current.id) {
return current.id.name
}
current = current.parent
}
return null
}
function isNamespacedProtobufType(protobufNamespaceImports, typeName) {
if (!typeName.includes(".")) {
return false
}
const namespace = typeName.split(".")[0]
return protobufNamespaceImports.has(namespace)
}
function findParentFunction(node) {
let current = node.parent
while (current) {
if (
current.type === "FunctionDeclaration" ||
current.type === "FunctionExpression" ||
current.type === "ArrowFunctionExpression"
) {
return current
}
current = current.parent
}
return null
}
+2477 -2477
View File
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
import { v4 as uuidv4 } from "uuid"
import { hostServiceHandlers } from "./host-grpc-service-config"
import { GrpcRequestRegistry } from "../../src/core/controller/grpc-request-registry"
/**
* Type definition for a streaming response handler
*/
export type StreamingResponseHandler = (response: any, isLast?: boolean, sequenceNumber?: number) => Promise<void>
// Registry to track active gRPC requests and their cleanup functions
const requestRegistry = new GrpcRequestRegistry()
/**
* Callback interface for streaming requests
*/
export interface StreamingCallbacks<T = any> {
onResponse: (response: T) => void
onError?: (error: Error) => void
onComplete?: () => void
}
/**
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor() {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @param streamingCallbacks Optional callbacks for streaming responses
* @returns For unary requests: the response message or error. For streaming requests: a cancel function.
*/
async handleRequest<T = any>(
service: string,
method: string,
message: any,
requestId: string,
streamingCallbacks?: StreamingCallbacks<T>,
): Promise<
| {
message?: any
error?: string
request_id: string
}
| (() => void)
> {
// If streaming callbacks are provided, handle as a streaming request
if (streamingCallbacks) {
let completionCalled = false
// Create a response handler that will call the client's callbacks
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
try {
// Call the client's onResponse callback with the response
streamingCallbacks.onResponse(response)
// If this is the last response, call the onComplete callback
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
} catch (error) {
// If there's an error in the callback, call the onError callback
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
}
// Register the response handler with the registry
requestRegistry.registerRequest(
requestId,
() => {
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
if (streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
},
{ type: "streaming_request", service, method },
responseHandler,
)
// Call the streaming handler directly
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
try {
await this.handleStreamingRequest(service, method, message, requestId)
} catch (error) {
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
// Return a function to cancel the stream
return () => {
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
this.cancelRequest(requestId)
}
}
// Handle as a unary request
try {
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Handle unary request
return {
message: await serviceConfig.requestHandler(method, message),
request_id: requestId,
}
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
/**
* Cancel a gRPC request
* @param requestId The request ID to cancel
* @returns True if the request was found and cancelled, false otherwise
*/
public async cancelRequest(requestId: string): Promise<boolean> {
const cancelled = requestRegistry.cancelRequest(requestId)
if (cancelled) {
// Get the registered response handler from the registry
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (requestInfo && requestInfo.responseStream) {
try {
// Send cancellation confirmation using the registered response handler
await requestInfo.responseStream(
{ cancelled: true },
true, // Mark as last message
)
} catch (e) {
console.error(`Error sending cancellation response for ${requestId}:`, e)
}
}
} else {
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
}
return cancelled
}
/**
* Handle a streaming gRPC request
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
*/
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Check if the service supports streaming
if (!serviceConfig.streamingHandler) {
throw new Error(`Service ${service} does not support streaming`)
}
// Get the registered response handler from the registry
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (!requestInfo || !requestInfo.responseStream) {
throw new Error(`No response handler registered for request: ${requestId}`)
}
// Use the registered response handler
const responseStream = requestInfo.responseStream
// Handle streaming request and pass the requestId to all streaming handlers
await serviceConfig.streamingHandler(method, message, responseStream, requestId)
// Don't send a final message here - the stream should stay open for future updates
// The stream will be closed when the client disconnects or when the service explicitly ends it
}
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}
+138
View File
@@ -0,0 +1,138 @@
import { StreamingResponseHandler } from "./host-grpc-handler"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (message: any) => Promise<any>
/**
* Type for streaming method handlers
*/
export type StreamingMethodHandler = (message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>
/**
* Method metadata including streaming information
*/
export interface MethodMetadata {
isStreaming: boolean
}
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
private streamingMethodRegistry: Record<string, StreamingMethodHandler> = {}
private methodMetadata: Record<string, MethodMetadata> = {}
/**
* Create a new service registry
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
this.serviceName = serviceName
}
/**
* Register a method handler
* @param methodName The name of the method to register
* @param handler The handler function for the method
* @param metadata Optional metadata about the method
*/
registerMethod(methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata): void {
const isStreaming = metadata?.isStreaming || false
if (isStreaming) {
this.streamingMethodRegistry[methodName] = handler as StreamingMethodHandler
} else {
this.methodRegistry[methodName] = handler as ServiceMethodHandler
}
this.methodMetadata[methodName] = { isStreaming, ...metadata }
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
}
/**
* Check if a method is a streaming method
* @param method The method name
* @returns True if the method is a streaming method
*/
isStreamingMethod(method: string): boolean {
return this.methodMetadata[method]?.isStreaming || false
}
/**
* Get a streaming method handler
* @param method The method name
* @returns The streaming method handler or undefined if not found
*/
getStreamingHandler(method: string): StreamingMethodHandler | undefined {
return this.streamingMethodRegistry[method]
}
/**
* Handle a service request
* @param method The method name
* @param message The request message
* @returns The response message
*/
async handleRequest(method: string, message: any): Promise<any> {
const handler = this.methodRegistry[method]
if (!handler) {
if (this.isStreamingMethod(method)) {
throw new Error(`Method ${method} is a streaming method and should be handled with handleStreamingRequest`)
}
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
}
return handler(message)
}
/**
* Handle a streaming service request
* @param method The method name
* @param message The request message
* @param responseStream The streaming response handler
* @param requestId The request ID for correlation and cleanup
*/
async handleStreamingRequest(
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const handler = this.streamingMethodRegistry[method]
if (!handler) {
if (this.methodRegistry[method]) {
throw new Error(`Method ${method} is not a streaming method and should be handled with handleRequest`)
}
throw new Error(`Unknown ${this.serviceName} streaming method: ${method}`)
}
await handler(message, responseStream, requestId)
}
}
/**
* Create a service registry factory function
* @param serviceName The name of the service
* @returns An object with register and handle functions
*/
export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata) =>
registry.registerMethod(methodName, handler, metadata),
handleRequest: (method: string, message: any) => registry.handleRequest(method, message),
handleStreamingRequest: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) =>
registry.handleStreamingRequest(method, message, responseStream, requestId),
isStreamingMethod: (method: string) => registry.isStreamingMethod(method),
}
}
+20
View File
@@ -0,0 +1,20 @@
import * as vscode from "vscode"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Creates a file URI from a file path
* @param request The request containing the file path
* @returns A URI object representing the file
*/
export async function file(request: StringRequest): Promise<Uri> {
const uri = vscode.Uri.file(request.value)
return Uri.create({
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
query: uri.query,
fragment: uri.fragment,
fsPath: uri.fsPath,
})
}
+28
View File
@@ -0,0 +1,28 @@
import * as vscode from "vscode"
import { JoinPathRequest, Uri } from "../../../src/shared/proto/host/uri"
/**
* Joins a URI with additional path segments
* @param request The request containing the base URI and path segments
* @returns A new URI with the path segments joined
*/
export async function joinPath(request: JoinPathRequest): Promise<Uri> {
// Convert proto Uri to vscode.Uri
if (!request.base) {
throw new Error("Base URI is required")
}
const baseUri = vscode.Uri.parse(`${request.base.scheme}://${request.base.authority}${request.base.path}`)
// Join paths
const result = vscode.Uri.joinPath(baseUri, ...request.pathSegments)
// Convert back to proto Uri
return Uri.create({
scheme: result.scheme,
authority: result.authority,
path: result.path,
query: result.query,
fragment: result.fragment,
fsPath: result.fsPath,
})
}
+20
View File
@@ -0,0 +1,20 @@
import * as vscode from "vscode"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Parses a string URI into a Uri object
* @param request The request containing the URI string
* @returns A URI object representing the parsed URI
*/
export async function parse(request: StringRequest): Promise<Uri> {
const uri = vscode.Uri.parse(request.value)
return Uri.create({
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
query: uri.query,
fragment: uri.fragment,
fsPath: uri.fsPath,
})
}
+225
View File
@@ -0,0 +1,225 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "../../../src/shared/proto/host/watch"
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
// Debounce configuration
const DEBOUNCE_DELAY = 100 // ms
// Keep track of active file watchers
const fileWatchers = new Map<
string,
{
watcher: fsSync.FSWatcher
subscribers: Set<StreamingResponseHandler>
lastEventTime: Map<FileChangeEvent_ChangeType, number> // Track last event time by event type
}
>()
/**
* Subscribe to file changes
* @param request The request containing the file path
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToFile(
request: SubscribeToFileRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const filePath = request.path
console.log(`[DEBUG] Setting up file subscription for ${filePath}`)
try {
// We don't send an initial event to avoid triggering handlers immediately
console.log(`[DEBUG] Now watching file: ${filePath}`)
// Set up or reuse file watcher
if (!fileWatchers.has(filePath)) {
// Create a new watcher for this file using Node.js fs.watch API
// This is more reliable than the VSCode FileSystemWatcher for detecting file saves
const watcher = fsSync.watch(filePath, { persistent: true }, async (eventType, filename) => {
if (eventType === "change") {
try {
const content = await fs.readFile(filePath, "utf8")
console.log(`[DEBUG] File changed: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.CHANGED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing change event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content,
})
} catch (error) {
console.error(`Error sending file change event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
}
} catch (error) {
console.error(`Error reading changed file: ${error}`)
}
} else if (eventType === "rename") {
// In Node.js fs.watch, 'rename' can mean either creation or deletion
// We need to check if the file exists to determine which it is
try {
await fs.access(filePath)
// File exists, so it was created or renamed
const content = await fs.readFile(filePath, "utf8")
console.log(`[DEBUG] File created/renamed: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.CREATED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing creation event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content,
})
} catch (error) {
console.error(`Error sending file creation event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
}
} catch (error) {
// File doesn't exist, so it was deleted
console.log(`[DEBUG] File deleted: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.DELETED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing deletion event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content: "",
})
} catch (error) {
console.error(`Error sending file deletion event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
// Clean up the watcher
cleanupWatcher(filePath)
}
}
}
})
// Set up the watcher info
const watcherInfo = {
watcher,
subscribers: new Set<StreamingResponseHandler>(),
lastEventTime: new Map<FileChangeEvent_ChangeType, number>(),
}
fileWatchers.set(filePath, watcherInfo)
}
// Add this subscriber to the watcher
const watcherInfo = fileWatchers.get(filePath)!
watcherInfo.subscribers.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
console.log(`[DEBUG] Cleaning up file subscription for ${filePath}`)
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
watcherInfo.subscribers.delete(responseStream)
// If no subscribers left, clean up the watcher
if (watcherInfo.subscribers.size === 0) {
cleanupWatcher(filePath)
}
}
}
// Register the cleanup function with the request registry
if (requestId) {
getRequestRegistry().registerRequest(
requestId,
cleanup,
{ type: "file_subscription", path: filePath },
responseStream,
)
}
} catch (error) {
console.error(`Error setting up file subscription: ${error}`)
// Send an error response
await responseStream({
path: filePath,
type: FileChangeEvent_ChangeType.DELETED,
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
})
}
}
/**
* Clean up a file watcher
* @param filePath The path of the file to clean up
*/
function cleanupWatcher(filePath: string): void {
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
watcherInfo.watcher.close()
fileWatchers.delete(filePath)
console.log(`[DEBUG] Removed file watcher for ${filePath}`)
}
}
+1778 -1571
View File
File diff suppressed because it is too large Load Diff
+61 -2
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.17.8",
"version": "3.17.11",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -46,6 +46,58 @@
],
"main": "./dist/extension.js",
"contributes": {
"walkthroughs": [
{
"id": "ClineWalkthrough",
"title": "Meet Cline, your new coding partner",
"description": "Cline codes like a developer because it thinks like one. Here are 5 ways to put it to work:",
"steps": [
{
"id": "welcome",
"title": "Start with a Goal, Not Just a Prompt",
"description": "Tell Cline what you want to achieve. It plans, asks, and then codes, like a true partner.",
"media": {
"markdown": "walkthrough/step1.md"
}
},
{
"id": "learn",
"title": "Let Cline Learn Your Codebase",
"description": "Point Cline to your project. It builds understanding to make smart, context-aware changes.",
"media": {
"markdown": "walkthrough/step2.md"
}
},
{
"id": "advanced-features",
"title": "Always Use the Best AI Models",
"description": "Cline empowers you with State-of-the-Art AI, connecting to top models (Anthropic, Gemini, OpenAI & more).",
"media": {
"markdown": "walkthrough/step3.md"
}
},
{
"id": "mcp",
"title": "Extend with Powerful Tools (MCP)",
"description": "Connect to databases, APIs, or discover new capabilities in the MCP Marketplace.",
"media": {
"markdown": "walkthrough/step4.md"
}
},
{
"id": "getting-started",
"title": "You're Always in Control",
"description": "Review Cline's plans and diffs. Approve changes before they happen. No surprises.",
"media": {
"markdown": "walkthrough/step5.md"
},
"content": {
"path": "walkthrough/step5.md"
}
}
]
}
],
"viewsContainers": {
"activitybar": [
{
@@ -143,6 +195,11 @@
"command": "cline.improveCode",
"title": "Improve with Cline",
"category": "Cline"
},
{
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
"category": "Cline"
}
],
"keybindings": [
@@ -343,7 +400,7 @@
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@aws-sdk/client-bedrock-runtime": "^3.821.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -395,12 +452,14 @@
"posthog-node": "^4.8.1",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
"vscode-uri": "^3.1.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
}
+253 -36
View File
@@ -12,38 +12,6 @@ import { createRequire } from "module"
const require = createRequire(import.meta.url)
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
if (process.platform !== "darwin") {
return
}
// Check if running on Apple Silicon
const cpuArchitecture = os.arch()
if (cpuArchitecture === "arm64") {
try {
// Check if Rosetta is installed
const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim()
if (rosettaCheck === "NOT_INSTALLED") {
console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture."))
console.log(
chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."),
)
console.log(chalk.cyan("Please install Rosetta 2 using the following command:"))
console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license"))
console.log(chalk.red("Aborting build process."))
process.exit(1)
} else {
console.log(chalk.green("Rosetta 2 is installed. Continuing with build."))
}
} catch (error) {
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
}
}
}
const __filename = fileURLToPath(import.meta.url)
const SCRIPT_DIR = path.dirname(__filename)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
@@ -72,6 +40,15 @@ const serviceNameMap = {
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
// Add new host services here
}
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "hosts", "vscode", serviceKey))
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -81,7 +58,7 @@ async function main() {
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directory if it doesn't exist
// Create output directories if they don't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
@@ -96,7 +73,7 @@ async function main() {
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, realpath: true })
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
// Build the protoc command with proper path handling for cross-platform
const tsProtocCommand = [
@@ -104,6 +81,8 @@ async function main() {
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=exportCommonSymbols=false",
"--ts_proto_opt=outputIndex=true",
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
...protoFiles,
].join(" ")
@@ -117,7 +96,6 @@ async function main() {
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
await fs.mkdir(descriptorOutDir, { recursive: true })
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
const descriptorProtocCommand = [
protoc,
@@ -138,8 +116,11 @@ async function main() {
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
await generateMethodRegistrations()
await generateHostMethodRegistrations()
await generateServiceConfig()
await generateHostServiceConfig()
await generateGrpcClientConfig()
await generateHostGrpcClientConfig()
}
/**
@@ -282,7 +263,7 @@ async function generateMethodRegistrations() {
// Import all method implementations
import { registerMethod } from "./index"\n`
// Add imports for all implementation files
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
@@ -456,6 +437,242 @@ service ${serviceClassName} {
}
}
/**
* Generate method registration files for host services
*/
async function generateHostMethodRegistrations() {
console.log(chalk.cyan("Generating host method registration files..."))
// Parse proto files for streaming methods
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
for (const serviceDir of hostServiceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
await fs.mkdir(serviceDir, { recursive: true })
}
const serviceName = path.basename(serviceDir)
const registryFile = path.join(serviceDir, "methods.ts")
const indexFile = path.join(serviceDir, "index.ts")
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
console.log(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"\n`
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
}
// Add streaming methods information
if (streamingMethods.length > 0) {
methodsContent += `\n// Streaming methods for this service
export const streamingMethods = ${JSON.stringify(
streamingMethods.map((m) => m.name),
null,
2,
)}\n`
}
// Add registration function
methodsContent += `\n// Register all ${serviceName} service methods
export function registerAllMethods(): void {
\t// Register each method with the registry\n`
// Add registration statements
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
const isStreaming = streamingMethods.some((m) => m.name === baseName)
if (isStreaming) {
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
} else {
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
}
}
// Close the function
methodsContent += `}`
// Write the methods.ts file
await fs.writeFile(registryFile, methodsContent)
console.log(chalk.green(`Generated ${registryFile}`))
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create ${serviceName} service registry
const ${serviceName}Service = createServiceRegistry("${serviceName}")
// Export the method handler types and registration function
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
export const registerMethod = ${serviceName}Service.registerMethod
// Export the request handlers
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
// Register all ${serviceName} methods
registerAllMethods()`
// Write the index.ts file
await fs.writeFile(indexFile, indexContent)
console.log(chalk.green(`Generated ${indexFile}`))
}
console.log(chalk.green("Host method registration files generated successfully."))
}
/**
* Generate a service configuration file for host services
*/
async function generateHostServiceConfig() {
console.log(chalk.cyan("Generating host service configuration file..."))
const serviceImports = []
const serviceConfigs = []
// Add all services from the hostServiceNameMap
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
serviceImports.push(
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
requestHandler: handle${capitalizedName}ServiceRequest,
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
}`)
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
${serviceImports.join("\n")}
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>;
streamingHandler: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "hosts", "vscode", "host-grpc-service-config.ts")
await fs.mkdir(path.dirname(configPath), { recursive: true })
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated host service configuration at ${configPath}`))
}
/**
* Generate a gRPC client configuration file for host services
*/
async function generateHostGrpcClientConfig() {
console.log(chalk.cyan("Generating host gRPC client configuration..."))
const serviceImports = []
const serviceClientCreations = []
const serviceExports = []
// Process each service in the hostServiceNameMap
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
// Add import statement
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/host/${dirName}"`)
// Add client creation
serviceClientCreations.push(
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
)
// Add to exports
serviceExports.push(`${capitalizedName}ServiceClient`)
}
// Generate the file content
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
${serviceImports.join("\n")}
${serviceClientCreations.join("\n")}
export {
${serviceExports.join(",\n\t")}
}`
const configPath = path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts")
await fs.mkdir(path.dirname(configPath), { recursive: true })
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated host gRPC client at ${configPath}`))
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
if (process.platform !== "darwin") {
return
}
// Check if running on Apple Silicon
const cpuArchitecture = os.arch()
if (cpuArchitecture === "arm64") {
try {
// Check if Rosetta is installed
const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim()
if (rosettaCheck === "NOT_INSTALLED") {
console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture."))
console.log(
chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."),
)
console.log(chalk.cyan("Please install Rosetta 2 using the following command:"))
console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license"))
console.log(chalk.red("Aborting build process."))
process.exit(1)
}
} catch (error) {
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
}
}
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
+5
View File
@@ -63,3 +63,8 @@ message StringArrays {
repeated string values1 = 1;
repeated string values2 = 2;
}
message KeyValuePair {
string key = 1;
string value = 2;
}
+6
View File
@@ -52,6 +52,12 @@ service FileService {
// Refreshes all rule toggles (Cline, External, and Workflows)
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
// Opens a task's conversation history file on disk
rpc openTaskHistory(StringRequest) returns (Empty);
// Subscribe to workspace file updates
rpc subscribeToWorkspaceUpdates(EmptyRequest) returns (stream StringArray);
}
// Response for refreshRules operation
+36
View File
@@ -0,0 +1,36 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// UriService provides methods for working with URIs in the IDE
service UriService {
// Create a new file URI from a file path
rpc file(cline.StringRequest) returns (Uri);
// Join a URI with additional path segments
rpc joinPath(JoinPathRequest) returns (Uri);
// Parse a string URI into a Uri object
rpc parse(cline.StringRequest) returns (Uri);
}
// Uri represents a URI in the IDE
message Uri {
string scheme = 1;
string authority = 2;
string path = 3;
string query = 4;
string fragment = 5;
string fsPath = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string pathSegments = 3;
}
+32
View File
@@ -0,0 +1,32 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// WatchService provides methods for watching files in the IDE
service WatchService {
// Subscribe to file changes
rpc subscribeToFile(SubscribeToFileRequest) returns (stream FileChangeEvent);
}
// Request to subscribe to file changes
message SubscribeToFileRequest {
cline.Metadata metadata = 1;
string path = 2;
}
// Event representing a file change
message FileChangeEvent {
enum ChangeType {
CREATED = 0;
CHANGED = 1;
DELETED = 2;
}
string path = 1;
ChangeType type = 2;
string content = 3; // Optional content of the file after change
}
+3
View File
@@ -16,6 +16,9 @@ service McpService {
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
}
message ToggleMcpServerRequest {
+35 -8
View File
@@ -20,6 +20,8 @@ service ModelsService {
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -35,17 +37,42 @@ message VsCodeLmModel {
string id = 4;
}
// Price tier for tiered pricing models
message PriceTier {
int32 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
double price = 2; // Price per million tokens for this tier
}
// Thinking configuration for models that support thinking/reasoning
message ThinkingConfig {
optional int32 max_budget = 1; // Max allowed thinking budget tokens
optional double output_price = 2; // Output price per million tokens when budget > 0
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
}
// Model tier for tiered pricing structures
message ModelTier {
int32 context_window = 1;
optional double input_price = 2;
optional double output_price = 3;
optional double cache_writes_price = 4;
optional double cache_reads_price = 5;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
bool supports_prompt_cache = 4;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
optional double input_price = 5;
optional double output_price = 6;
optional double cache_writes_price = 7;
optional double cache_reads_price = 8;
optional string description = 9;
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
}
// Shared response message for model information
+124
View File
@@ -13,6 +13,7 @@ service StateService {
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
}
message State {
@@ -64,3 +65,126 @@ message AutoApprovalSettingsRequest {
bool enable_notifications = 6;
repeated string favorites = 7;
}
// Message for updating settings
message UpdateSettingsRequest {
Metadata metadata = 1;
optional ApiConfiguration api_configuration = 2;
optional string custom_instructions_setting = 3;
optional string telemetry_setting = 4;
optional bool plan_act_separate_models_setting = 5;
optional bool enable_checkpoints_setting = 6;
optional bool mcp_marketplace_enabled = 7;
optional ChatSettings chat_settings = 8;
optional int64 shell_integration_timeout = 9;
optional bool terminal_reuse_enabled = 10;
optional bool mcp_responses_collapsed = 11;
}
// Complete API Configuration message
message ApiConfiguration {
// Core API fields
optional string api_provider = 1;
optional string api_model_id = 2;
optional string api_key = 3; // anthropic
optional string api_base_url = 4;
// Provider-specific API keys
optional string cline_api_key = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
optional string openai_native_api_key = 9;
optional string gemini_api_key = 10;
optional string deepseek_api_key = 11;
optional string requesty_api_key = 12;
optional string together_api_key = 13;
optional string fireworks_api_key = 14;
optional string qwen_api_key = 15;
optional string doubao_api_key = 16;
optional string mistral_api_key = 17;
optional string nebius_api_key = 18;
optional string asksage_api_key = 19;
optional string xai_api_key = 20;
optional string sambanova_api_key = 21;
optional string cerebras_api_key = 22;
// Model IDs
optional string openrouter_model_id = 23;
optional string openai_model_id = 24;
optional string anthropic_model_id = 25;
optional string bedrock_model_id = 26;
optional string vertex_model_id = 27;
optional string gemini_model_id = 28;
optional string ollama_model_id = 29;
optional string lm_studio_model_id = 30;
optional string litellm_model_id = 31;
optional string requesty_model_id = 32;
optional string together_model_id = 33;
optional string fireworks_model_id = 34;
// AWS Bedrock fields
optional bool aws_bedrock_custom_selected = 35;
optional string aws_bedrock_custom_model_base_id = 36;
optional string aws_access_key = 37;
optional string aws_secret_key = 38;
optional string aws_session_token = 39;
optional string aws_region = 40;
optional bool aws_use_cross_region_inference = 41;
optional bool aws_bedrock_use_prompt_cache = 42;
optional bool aws_use_profile = 43;
optional string aws_profile = 44;
optional string aws_bedrock_endpoint = 45;
// Vertex AI fields
optional string vertex_project_id = 46;
optional string vertex_region = 47;
// Base URLs and endpoints
optional string openai_base_url = 48;
optional string ollama_base_url = 49;
optional string lm_studio_base_url = 50;
optional string gemini_base_url = 51;
optional string litellm_base_url = 52;
optional string asksage_api_url = 53;
// LiteLLM specific fields
optional string litellm_api_key = 54;
optional bool litellm_use_prompt_cache = 55;
// Model configuration
optional int64 thinking_budget_tokens = 56;
optional string reasoning_effort = 57;
optional int64 request_timeout_ms = 58;
// Fireworks specific
optional int64 fireworks_model_max_completion_tokens = 59;
optional int64 fireworks_model_max_tokens = 60;
// Azure specific
optional string azure_api_version = 61;
// Ollama specific
optional string ollama_api_options_ctx_num = 62;
// Qwen specific
optional string qwen_api_line = 63;
// OpenRouter specific
optional string openrouter_provider_sorting = 64;
// VSCode LM (stored as JSON string due to complex type)
optional string vscode_lm_model_selector = 65;
// Model info objects (stored as JSON strings)
optional string openrouter_model_info = 66;
optional string openai_model_info = 67;
optional string requesty_model_info = 68;
optional string litellm_model_info = 69;
// OpenAI headers (stored as JSON string)
optional string openai_headers = 70;
// Favorited model IDs
repeated string favorited_model_ids = 71;
}
+9
View File
@@ -33,6 +33,8 @@ service TaskService {
rpc taskFeedback(StringRequest) returns (Empty);
// Shows task completion changes diff in a view
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
// Executes a quick win task with command and title
rpc executeQuickWin(ExecuteQuickWinRequest) returns (Empty);
}
// Request message for creating a new task
@@ -107,3 +109,10 @@ message AskResponseRequest {
repeated string images = 4;
repeated string files = 5;
}
// Request for executing a quick win task
message ExecuteQuickWinRequest {
Metadata metadata = 1;
string command = 2;
string title = 3;
}
+237 -1
View File
@@ -6,14 +6,250 @@ option java_multiple_files = true;
import "common.proto";
// Enum for webview provider types
enum WebviewProviderType {
SIDEBAR = 0;
TAB = 1;
}
// Define a new message type for webview provider info
message WebviewProviderTypeRequest {
Metadata metadata = 1;
WebviewProviderType providerType = 2;
}
// Enum for ClineMessage type
enum ClineMessageType {
ASK = 0;
SAY = 1;
}
// Enum for ClineAsk types
enum ClineAsk {
FOLLOWUP = 0;
PLAN_MODE_RESPOND = 1;
COMMAND = 2;
COMMAND_OUTPUT = 3;
COMPLETION_RESULT = 4;
TOOL = 5;
API_REQ_FAILED = 6;
RESUME_TASK = 7;
RESUME_COMPLETED_TASK = 8;
MISTAKE_LIMIT_REACHED = 9;
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
BROWSER_ACTION_LAUNCH = 11;
USE_MCP_SERVER = 12;
NEW_TASK = 13;
CONDENSE = 14;
REPORT_BUG = 15;
}
// Enum for ClineSay types
enum ClineSay {
TASK = 0;
ERROR = 1;
API_REQ_STARTED = 2;
API_REQ_FINISHED = 3;
TEXT = 4;
REASONING = 5;
COMPLETION_RESULT_SAY = 6;
USER_FEEDBACK = 7;
USER_FEEDBACK_DIFF = 8;
API_REQ_RETRIED = 9;
COMMAND_SAY = 10;
COMMAND_OUTPUT_SAY = 11;
TOOL_SAY = 12;
SHELL_INTEGRATION_WARNING = 13;
BROWSER_ACTION_LAUNCH_SAY = 14;
BROWSER_ACTION = 15;
BROWSER_ACTION_RESULT = 16;
MCP_SERVER_REQUEST_STARTED = 17;
MCP_SERVER_RESPONSE = 18;
USE_MCP_SERVER_SAY = 19;
DIFF_ERROR = 20;
DELETED_API_REQS = 21;
CLINEIGNORE_ERROR = 22;
CHECKPOINT_CREATED = 23;
LOAD_MCP_DOCUMENTATION = 24;
INFO = 25;
}
// Enum for ClineSayTool tool types
enum ClineSayToolType {
EDITED_EXISTING_FILE = 0;
NEW_FILE_CREATED = 1;
READ_FILE = 2;
LIST_FILES_TOP_LEVEL = 3;
LIST_FILES_RECURSIVE = 4;
LIST_CODE_DEFINITION_NAMES = 5;
SEARCH_FILES = 6;
WEB_FETCH = 7;
}
// Enum for browser actions
enum BrowserAction {
LAUNCH = 0;
CLICK = 1;
TYPE = 2;
SCROLL_DOWN = 3;
SCROLL_UP = 4;
CLOSE = 5;
}
// Enum for MCP server request types
enum McpServerRequestType {
USE_MCP_TOOL = 0;
ACCESS_MCP_RESOURCE = 1;
}
// Enum for API request cancel reasons
enum ClineApiReqCancelReason {
STREAMING_FAILED = 0;
USER_CANCELLED = 1;
RETRIES_EXHAUSTED = 2;
}
// Message for conversation history deleted range
message ConversationHistoryDeletedRange {
int32 start_index = 1;
int32 end_index = 2;
}
// Message for ClineSayTool
message ClineSayTool {
ClineSayToolType tool = 1;
string path = 2;
string diff = 3;
string content = 4;
string regex = 5;
string file_pattern = 6;
bool operation_is_located_in_workspace = 7;
}
// Message for ClineSayBrowserAction
message ClineSayBrowserAction {
BrowserAction action = 1;
string coordinate = 2;
string text = 3;
}
// Message for BrowserActionResult
message BrowserActionResult {
string screenshot = 1;
string logs = 2;
string current_url = 3;
string current_mouse_position = 4;
}
// Message for ClineAskUseMcpServer
message ClineAskUseMcpServer {
string server_name = 1;
McpServerRequestType type = 2;
string tool_name = 3;
string arguments = 4;
string uri = 5;
}
// Message for ClinePlanModeResponse
message ClinePlanModeResponse {
string response = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskQuestion
message ClineAskQuestion {
string question = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskNewTask
message ClineAskNewTask {
string context = 1;
}
// Message for API request retry status
message ApiReqRetryStatus {
int32 attempt = 1;
int32 max_attempts = 2;
int32 delay_sec = 3;
string error_snippet = 4;
}
// Message for ClineApiReqInfo
message ClineApiReqInfo {
string request = 1;
int32 tokens_in = 2;
int32 tokens_out = 3;
int32 cache_writes = 4;
int32 cache_reads = 5;
double cost = 6;
ClineApiReqCancelReason cancel_reason = 7;
string streaming_failed_message = 8;
ApiReqRetryStatus retry_status = 9;
}
// Main ClineMessage type
message ClineMessage {
int64 ts = 1;
ClineMessageType type = 2;
ClineAsk ask = 3;
ClineSay say = 4;
string text = 5;
string reasoning = 6;
repeated string images = 7;
repeated string files = 8;
bool partial = 9;
string last_checkpoint_hash = 10;
bool is_checkpoint_checked_out = 11;
bool is_operation_outside_workspace = 12;
int32 conversation_history_index = 13;
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
// Additional fields for specific ask/say types
ClineSayTool say_tool = 15;
ClineSayBrowserAction say_browser_action = 16;
BrowserActionResult browser_action_result = 17;
ClineAskUseMcpServer ask_use_mcp_server = 18;
ClinePlanModeResponse plan_mode_response = 19;
ClineAskQuestion ask_question = 20;
ClineAskNewTask ask_new_task = 21;
ClineApiReqInfo api_req_info = 22;
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
rpc scrollToSettings(StringRequest) returns (Empty);
rpc scrollToSettings(StringRequest) returns (KeyValuePair);
// Marks the current announcement as shown and returns whether an announcement should still be shown
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
// Subscribe to addToInput events (when user adds content via context menu)
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
// Subscribe to MCP button clicked events
rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to history button click events
rpc subscribeToHistoryButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to account button click events
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to settings button clicked events
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
}
+5 -3
View File
@@ -32,13 +32,15 @@ function generateHandlersAndExports() {
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
const requestType = "proto.cline." + rpc.requestType.type.name
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
}
if (rpc.responseStream) {
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
} else {
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
const responseType = "proto.cline." + rpc.responseType.type.name
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
}
}
handlerSetup.push(` });`)
@@ -58,11 +60,11 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
let output = `// GENERATED CODE -- DO NOT EDIT!
// Generated by ${scriptName}
import * as grpc from "@grpc/grpc-js"
import * as proto from "@/shared/proto"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
${imports}
export function addServices(
server: grpc.Server,
proto: any,
+1 -1
View File
@@ -39,7 +39,7 @@ const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 9 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("error", (err) => {
+2
View File
@@ -9,6 +9,7 @@ import {
askSageDefaultURL,
} from "@shared/api"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
type AskSageRequest = {
system_prompt: string
@@ -45,6 +46,7 @@ export class AskSageHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const model = this.getModel()
+2
View File
@@ -6,6 +6,7 @@ import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import axios from "axios"
import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -25,6 +26,7 @@ export class ClineHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
this.lastGenerationId = undefined
+2
View File
@@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -28,6 +29,7 @@ export class DoubaoHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+16 -2
View File
@@ -171,8 +171,22 @@ export class GeminiHandler implements ApiHandler {
// Gemini doesn't include status codes in their errors
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
;(error as any).status = 429
const rateLimitPatterns = [
/got status: 429/i,
/429 Too Many Requests/i,
/rate limit exceeded/i,
/too many requests/i,
]
const isRateLimit =
error.name === "ClientError" && rateLimitPatterns.some((pattern) => pattern.test(error.message))
if (isRateLimit) {
const rateLimitError = Object.assign(new Error(error.message), {
...error,
status: 429,
})
throw rateLimitError
}
} else {
apiError = String(error)
+2
View File
@@ -4,6 +4,7 @@ import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { withRetry } from "../retry"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -51,6 +52,7 @@ export class LiteLlmHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
+8 -1
View File
@@ -4,6 +4,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -17,6 +18,7 @@ export class LmStudioHandler implements ApiHandler {
})
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -27,7 +29,6 @@ export class LmStudioHandler implements ApiHandler {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
stream: true,
})
for await (const chunk of stream) {
@@ -38,6 +39,12 @@ export class LmStudioHandler implements ApiHandler {
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
}
} catch (error) {
// LM Studio doesn't return an error code/body for now
+21 -2
View File
@@ -14,6 +14,7 @@ import {
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { withRetry } from "../retry"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -48,23 +49,41 @@ export class QwenHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
let temperature: number | undefined = 0
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingArgs = isReasoningModelFamily
? {
enable_thinking: reasoningOn,
thinking_budget: reasoningOn ? budgetTokens : undefined,
}
: undefined
if (isDeepseekReasoner || (reasoningOn && isReasoningModelFamily)) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
temperature = undefined
}
const stream = await this.client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
temperature,
...thinkingArgs,
})
for await (const chunk of stream) {
+2
View File
@@ -7,6 +7,7 @@ import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
import { withRetry } from "../retry"
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
@@ -406,6 +407,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
return content
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Ensure clean state before starting a new request
this.ensureCleanState()
+2
View File
@@ -5,6 +5,7 @@ import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels
import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { withRetry } from "../retry"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -18,6 +19,7 @@ export class XAIHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
-127
View File
@@ -30,7 +30,6 @@ export class StreamingJsonReplacer {
private onErrorCallback: (error: Error) => void
private itemsProcessed: number = 0
private successfullyParsedItems: ReplacementItem[] = []
private logFilePath: string
constructor(
initialContent: string,
@@ -39,212 +38,100 @@ export class StreamingJsonReplacer {
) {
// Initialize log file path
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
this.logFilePath = path.join(os.homedir(), "Documents", `streaming-json-replacer-debug-${timestamp}.log`)
// Initialize log file
this.log("StreamingJsonReplacer Debug Log Started", "INFO")
this.log("Timestamp: " + new Date().toISOString(), "INFO")
this.log("Constructor called with initial content length: " + initialContent.length, "INFO")
this.log("Initial content preview: " + initialContent.substring(0, 200) + "...", "INFO")
this.currentFileContent = initialContent
this.onContentUpdated = onContentUpdatedCallback
this.onErrorCallback = onErrorCallback
this.log("Initializing JSONParser with paths: ['$.*']", "INFO")
this.parser = new JSONParser({ paths: ["$.*"] })
this.parser.onValue = (parsedElementInfo: ParsedElementInfo) => {
this.log("onValue callback triggered")
this.log("parsedElementInfo: " + JSON.stringify(parsedElementInfo, null, 2))
const { value } = parsedElementInfo // Destructure to get value, which might be undefined
this.log("Extracted value: " + JSON.stringify(value))
this.log("Value type: " + typeof value)
// This callback is triggered for each item matched by '$.replacements.*'
if (value && typeof value === "object" && "old_string" in value && "new_string" in value) {
this.log("Found valid replacement item structure")
const item = value as ReplacementItem // Value here is confirmed to be an object
this.log("Replacement item: " + JSON.stringify(item, null, 2))
if (typeof item.old_string === "string" && typeof item.new_string === "string") {
this.log("Item has valid string types for old_string and new_string")
this.log("old_string length: " + item.old_string.length)
this.log("new_string length: " + item.new_string.length)
this.log(
"old_string preview: " +
(item.old_string.substring(0, 100) + (item.old_string.length > 100 ? "..." : "")),
)
this.log(
"new_string preview: " +
(item.new_string.substring(0, 100) + (item.new_string.length > 100 ? "..." : "")),
)
this.successfullyParsedItems.push(item) // Store the structurally valid item
this.log("Added item to successfullyParsedItems. Total count: " + this.successfullyParsedItems.length)
if (this.currentFileContent.includes(item.old_string)) {
this.log("old_string found in current file content - proceeding with replacement")
// Calculate the change location before making the replacement
const changeLocation = this.calculateChangeLocation(item.old_string, item.new_string)
this.log("Calculated change location: " + JSON.stringify(changeLocation))
const beforeLength = this.currentFileContent.length
this.currentFileContent = this.currentFileContent.replace(item.old_string, item.new_string)
const afterLength = this.currentFileContent.length
this.log("Content length before replacement: " + beforeLength)
this.log("Content length after replacement: " + afterLength)
this.log("Length difference: " + (afterLength - beforeLength))
this.itemsProcessed++
this.log("Incremented itemsProcessed to: " + this.itemsProcessed)
// Notify that an item has been processed. The `isFinalItem` argument here is tricky
// as we don't know from the parser alone if this is the *absolute* last item
// until the stream ends. The caller (Task.ts) will manage the final update.
// For now, we'll pass `false` and let Task.ts handle the final diff view update.
this.log("Calling onContentUpdated callback")
this.onContentUpdated(this.currentFileContent, false, changeLocation)
this.log("onContentUpdated callback completed")
} else {
this.log("old_string NOT found in current file content - generating error", "ERROR")
this.log("Current file content length: " + this.currentFileContent.length)
this.log("Current file content preview: " + this.currentFileContent.substring(0, 200) + "...")
const snippet = item.old_string.length > 50 ? item.old_string.substring(0, 47) + "..." : item.old_string
const error = new Error(`Streaming Replacement failed: 'old_string' not found. Snippet: "${snippet}"`)
this.log("Calling onErrorCallback with error: " + error.message, "ERROR")
this.onErrorCallback(error) // Call our own error callback
}
} else {
this.log(
"Invalid string types - old_string type: " +
typeof item.old_string +
", new_string type: " +
typeof item.new_string,
"ERROR",
)
const error = new Error(`Invalid item structure in replacements stream: ${JSON.stringify(item)}`)
this.log("Calling onErrorCallback with error: " + error.message, "ERROR")
this.onErrorCallback(error) // Call our own error callback
}
} else if (value && (Array.isArray(value) || (typeof value === "object" && "replacements" in value))) {
// This might be the 'replacements' array itself or the root object.
// The `paths: ['$.replacements.*']` should mean we only get items.
// If we get here, it's likely the root object if paths wasn't specific enough or if it's an empty replacements array.
this.log("Streaming parser emitted container: " + JSON.stringify(value))
this.log(
"Container type - isArray: " +
Array.isArray(value) +
", hasReplacements: " +
(typeof value === "object" && "replacements" in value),
)
} else {
// Value is not a ReplacementItem or a known container, could be an issue with the JSON structure or path.
// If `paths` is correct, this path should ideally not be hit often for valid streams.
this.log("Streaming parser emitted unexpected value: " + JSON.stringify(value), "WARN")
this.log("Unexpected value type: " + typeof value, "WARN")
this.log("Has old_string: " + (value && typeof value === "object" && "old_string" in value), "WARN")
this.log("Has new_string: " + (value && typeof value === "object" && "new_string" in value), "WARN")
}
}
this.parser.onError = (err: Error) => {
this.log("Parser onError callback triggered", "ERROR")
this.log("Error details: " + JSON.stringify(err), "ERROR")
this.log("Error message: " + err.message, "ERROR")
this.log("Error stack: " + err.stack, "ERROR")
// Propagate the error to the caller via the callback
this.log("Calling onErrorCallback with parser error", "ERROR")
this.onErrorCallback(err)
// Note: The @streamparser/json library might throw synchronously on write if onError is not set,
// or if it re-throws. We'll ensure Task.ts wraps write/end in try-catch.
}
this.log("Constructor completed - parser setup finished")
// Log to console where the debug file is located
console.log(`[StreamingJsonReplacer] Debug logging to file: ${this.logFilePath}`)
}
public write(jsonChunk: string): void {
this.log("write() called")
this.log("JSON chunk length: " + jsonChunk.length)
this.log("JSON chunk preview: " + jsonChunk.substring(0, 200) + (jsonChunk.length > 200 ? "..." : ""))
try {
// Errors during write will be caught by the parser's onError or thrown.
this.log("Calling parser.write()")
this.parser.write(jsonChunk)
this.log("parser.write() completed successfully")
} catch (error) {
this.log("Exception during parser.write(): " + error, "ERROR")
throw error
}
}
public getCurrentContent(): string {
this.log("getCurrentContent() called")
this.log("Current content length: " + this.currentFileContent.length)
return this.currentFileContent
}
public getSuccessfullyParsedItems(): ReplacementItem[] {
this.log("getSuccessfullyParsedItems() called")
this.log("Returning copy of " + this.successfullyParsedItems.length + " items")
return [...this.successfullyParsedItems] // Return a copy
}
private calculateChangeLocation(oldStr: string, newStr: string): ChangeLocation {
this.log("calculateChangeLocation() called")
this.log("oldStr length: " + oldStr.length)
this.log("newStr length: " + newStr.length)
this.log("oldStr preview: " + oldStr.substring(0, 50) + (oldStr.length > 50 ? "..." : ""))
this.log("newStr preview: " + newStr.substring(0, 50) + (newStr.length > 50 ? "..." : ""))
// Find the index where the old string starts
const startIndex = this.currentFileContent.indexOf(oldStr)
this.log("startIndex found: " + startIndex)
if (startIndex === -1) {
this.log("startIndex is -1 - old string not found in content!", "WARN")
this.log("This shouldn't happen since we already checked includes()", "WARN")
// This shouldn't happen since we already checked includes(), but just in case
return { startLine: 0, endLine: 0, startChar: 0, endChar: 0 }
}
// Calculate line numbers by counting newlines before the start index
const contentBeforeStart = this.currentFileContent.substring(0, startIndex)
this.log("contentBeforeStart length: " + contentBeforeStart.length)
const startLine = (contentBeforeStart.match(/\n/g) || []).length
this.log("calculated startLine: " + startLine)
// Calculate the end index after replacement
const endIndex = startIndex + oldStr.length
this.log("calculated endIndex: " + endIndex)
const contentBeforeEnd = this.currentFileContent.substring(0, endIndex)
this.log("contentBeforeEnd length: " + contentBeforeEnd.length)
const endLine = (contentBeforeEnd.match(/\n/g) || []).length
this.log("calculated endLine: " + endLine)
// Calculate character positions within their respective lines
const lastNewlineBeforeStart = contentBeforeStart.lastIndexOf("\n")
this.log("lastNewlineBeforeStart: " + lastNewlineBeforeStart)
const startChar = lastNewlineBeforeStart === -1 ? startIndex : startIndex - lastNewlineBeforeStart - 1
this.log("calculated startChar: " + startChar)
const lastNewlineBeforeEnd = contentBeforeEnd.lastIndexOf("\n")
this.log("lastNewlineBeforeEnd: " + lastNewlineBeforeEnd)
const endChar = lastNewlineBeforeEnd === -1 ? endIndex : endIndex - lastNewlineBeforeEnd - 1
this.log("calculated endChar: " + endChar)
const result = {
startLine,
@@ -253,20 +140,6 @@ export class StreamingJsonReplacer {
endChar,
}
this.log("calculateChangeLocation() returning: " + JSON.stringify(result))
return result
}
private log(message: string, level: "INFO" | "WARN" | "ERROR" = "INFO"): void {
const timestamp = new Date().toISOString()
const logLine = `[${timestamp}] [${level}] ${message}\n`
try {
fs.appendFileSync(this.logFilePath, logLine)
} catch (error) {
// Fallback to console if file logging fails
console.error("Failed to write to log file:", error)
console.log(`[${level}] ${message}`)
}
}
}
+28 -28
View File
@@ -11,55 +11,55 @@ describe("constructNewFileContent", () => {
{
name: "empty file",
original: "",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
=======
new content
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "new content\n",
isFinal: true,
},
{
name: "full file replacement",
original: "old content",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
=======
new content
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "new content\n",
isFinal: true,
},
{
name: "exact match replacement",
original: "line1\nline2\nline3",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
line2
=======
replaced
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "line-trimmed match replacement",
original: "line1\n line2 \nline3",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
line2
=======
replaced
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "block anchor match replacement",
original: "line1\nstart\nmiddle\nend\nline5",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
start
middle
end
=======
replaced
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line1\nreplaced\nline5",
isFinal: true,
},
@@ -67,11 +67,11 @@ replaced
name: "incremental processing",
original: "line1\nline2\nline3",
diff: [
`<<<<<<< SEARCH
`------- SEARCH
line2
=======`,
"replaced\n",
">>>>>>> REPLACE",
"+++++++ REPLACE",
].join("\n"),
expected: "line1\nreplaced\n\nline3",
isFinal: true,
@@ -79,60 +79,60 @@ line2
{
name: "final chunk with remaining content",
original: "line1\nline2\nline3",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
line2
=======
replaced
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3",
isFinal: true,
},
{
name: "multiple ordered replacements",
original: "First\nSecond\nThird\nFourth",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
First
=======
1st
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
------- SEARCH
Third
=======
3rd
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "1st\nSecond\n3rd\nFourth",
isFinal: true,
},
{
name: "replace then delete",
original: "line1\nline2\nline3\nline4",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
line2
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
------- SEARCH
line4
=======
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line1\nreplaced\nline3\n",
isFinal: true,
},
{
name: "delete then replace",
original: "line1\nline2\nline3\nline4",
diff: `<<<<<<< SEARCH
diff: `------- SEARCH
line1
=======
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
------- SEARCH
line3
=======
replaced
>>>>>>> REPLACE`,
+++++++ REPLACE`,
expected: "line2\nreplaced\nline4",
isFinal: true,
},
@@ -155,11 +155,11 @@ replaced
it("should throw error when no match found", async () => {
const original = "line1\nline2\nline3"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
non-existent
=======
replaced
>>>>>>> REPLACE`
+++++++ REPLACE`
try {
await cnfc(diff, original, true)
+28 -22
View File
@@ -1,3 +1,10 @@
const SEARCH_BLOCK_START = "------- SEARCH"
const SEARCH_BLOCK_END = "======="
const REPLACE_BLOCK_END = "+++++++ REPLACE"
const SEARCH_BLOCK_CHAR = "-"
const REPLACE_BLOCK_CHAR = "+"
/**
* Attempts a line-trimmed fallback match for the given search content in the original content.
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
@@ -150,11 +157,11 @@ function blockAnchorFallbackMatch(originalContent: string, searchContent: string
*
* The diff format is a custom structure that uses three markers to define changes:
*
* <<<<<<< SEARCH
* ------- SEARCH
* [Exact content to find in the original file]
* =======
* [Content to replace with]
* >>>>>>> REPLACE
* +++++++ REPLACE
*
* Behavior and Assumptions:
* 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
@@ -243,23 +250,23 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
lastLine !== "<<<<<<< SEARCH" &&
lastLine !== "=======" &&
lastLine !== ">>>>>>> REPLACE"
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
lastLine !== SEARCH_BLOCK_START &&
lastLine !== SEARCH_BLOCK_END &&
lastLine !== REPLACE_BLOCK_END
) {
lines.pop()
}
for (const line of lines) {
if (line === "<<<<<<< SEARCH") {
if (line === SEARCH_BLOCK_START) {
inSearch = true
currentSearchContent = ""
currentReplaceContent = ""
continue
}
if (line === "=======") {
if (line === SEARCH_BLOCK_END) {
inSearch = false
inReplace = true
@@ -320,7 +327,7 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
continue
}
if (line === ">>>>>>> REPLACE") {
if (line === REPLACE_BLOCK_END) {
// Finished one replace block
// // Remove the artificially added linebreak in the last line of the REPLACE block
@@ -480,7 +487,7 @@ class NewFileContentConstructor {
pendingNonStandardLineLimit: number,
): number {
let removeLineCount = 0
if (line === "<<<<<<< SEARCH") {
if (line === SEARCH_BLOCK_START) {
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
if (removeLineCount > 0) {
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
@@ -490,7 +497,7 @@ class NewFileContentConstructor {
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.activateSearchState()
} else if (line === "=======") {
} else if (line === SEARCH_BLOCK_END) {
// 校验非标内容
if (!this.isSearchingActive()) {
this.tryFixSearchBlock(pendingNonStandardLineLimit)
@@ -498,7 +505,7 @@ class NewFileContentConstructor {
}
this.activateReplaceState()
this.beforeReplace()
} else if (line === ">>>>>>> REPLACE") {
} else if (line === REPLACE_BLOCK_END) {
if (!this.isReplacingActive()) {
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
@@ -521,7 +528,6 @@ class NewFileContentConstructor {
} else {
let appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
console.log("unstandard line:" + line)
// 处理非标内容
this.pendingNonStandardLines.push(line)
}
@@ -606,11 +612,11 @@ class NewFileContentConstructor {
if (!lineLimit) {
throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
}
let searchTagRegexp = /^[<]{3,} SEARCH$/
let searchTagRegexp = /^[-]{3,} SEARCH$/
const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
if (searchTagIndex !== -1) {
let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
fixLines[0] = "<<<<<<< SEARCH"
fixLines[0] = SEARCH_BLOCK_START
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
}
@@ -638,7 +644,7 @@ class NewFileContentConstructor {
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[0] = "======="
fixLines[0] = SEARCH_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
}
@@ -657,7 +663,7 @@ class NewFileContentConstructor {
throw new Error()
}
let replaceEndTagRegexp = /^[>]{3,} REPLACE$/
let replaceEndTagRegexp = /^[+]{3,} REPLACE$/
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
if (likeReplaceEndTag) {
@@ -666,7 +672,7 @@ class NewFileContentConstructor {
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[fixLines.length - 1] = ">>>>>>> REPLACE"
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
}
@@ -706,10 +712,10 @@ export async function constructNewFileContentV2(diffContent: string, originalCon
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
lastLine !== "<<<<<<< SEARCH" &&
lastLine !== "=======" &&
lastLine !== ">>>>>>> REPLACE"
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
lastLine !== SEARCH_BLOCK_START &&
lastLine !== SEARCH_BLOCK_END &&
lastLine !== REPLACE_BLOCK_END
) {
lines.pop()
}
@@ -7,122 +7,122 @@ async function cnfc(diffContent: string, originalContent: string, isFinal: boole
}
describe("Diff Format Edge Cases", () => {
it("should handle SEARCH prefix symbols < less than 7", async () => {
it("should handle SEARCH prefix symbols - less than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<< SEARCH
const diff = `----- SEARCH
content
=======
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("new content\n")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH prefix symbols < more than 7", async () => {
it("should handle SEARCH prefix symbols - more than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<<<<<<<< SEARCH
const diff = `----------- SEARCH
content
=======
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("new content\n")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH < less than 7 and REPLACE = less than 7", async () => {
it("should handle SEARCH - less than 7 and REPLACE = less than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<< SEARCH
const diff = `----- SEARCH
content
=====
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH < less than 7 and REPLACE = more than 7", async () => {
it("should handle SEARCH - less than 7 and REPLACE = more than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<< SEARCH
const diff = `----- SEARCH
content
========
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH < more than 7 and REPLACE = more than 7", async () => {
it("should handle SEARCH - more than 7 and REPLACE = more than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<<<<<<<< SEARCH
const diff = `----------- SEARCH
content
==========
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH < more than 7 and REPLACE = less than 7", async () => {
it("should handle SEARCH - more than 7 and REPLACE = less than 7", async () => {
const isFinal = true
const original = "before\ncontent\nafter"
const diff = `<<<<<<<<<<< SEARCH
const diff = `----------- SEARCH
content
=====
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle consecutive SEARCH-REPLACE with second block SEARCH < less than 7", async () => {
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7", async () => {
const isFinal = true
const original = "before\nfirst content\nafter\nsecond content\nend"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
first content
=======
first new content
>>>>>>> REPLACE
<<<<< SEARCH
+++++++ REPLACE
----- SEARCH
second content
=======
second new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("before\nfirst new content\nsecond new content\n")
expect(result2).to.equal("before\nfirst new content\nafter\nsecond new content\nend")
})
it("should handle consecutive SEARCH-REPLACE with second block SEARCH < less than 7 and REPLACE = less than 7", async () => {
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7 and REPLACE = less than 7", async () => {
const isFinal = true
const original = "before\nfirst content\nafter\nsecond content\nend"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
first content
=======
first new content
>>>>>>> REPLACE
<<<<< SEARCH
+++++++ REPLACE
----- SEARCH
second content
=====
second new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("before\nfirst new content\nd")
@@ -11,7 +11,7 @@ describe("Diff Format Edge Cases", () => {
const original = "line1\nline2"
const diff = `=======
new content
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("new content\n")
try {
@@ -24,14 +24,14 @@ new content
it("should handle consecutive search blocks", async () => {
const original = "text"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
=======
replaced
>>>>>>> REPLACE
<<<<<<< SEARCH
+++++++ REPLACE
------- SEARCH
=======
another
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("replaced\nanother\n")
try {
@@ -44,10 +44,10 @@ another
it("should handle reverse markers order", async () => {
const original = "content"
const diff = `>>>>>>> SEARCH
const diff = `+++++++ SEARCH
=======
invalid
<<<<<<< REPLACE`
------- REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("invalid\ncontent")
try {
@@ -60,9 +60,9 @@ invalid
it("should handle incomplete block structure", async () => {
const original = "valid text"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
text
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("t")
try {
@@ -75,10 +75,10 @@ text
it("should handle empty search block", async () => {
const original = "any content"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
=======
inserted
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("inserted\n")
@@ -87,11 +87,11 @@ inserted
it("should handle mixed line endings", async () => {
const original = "line1\r\nline2"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
line1\r
=======
line1
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("line1\nline2")
@@ -100,11 +100,11 @@ line1
it("should handle special characters in search", async () => {
const original = "text with $^.*\nend"
const diff = `<<<<<<< SEARCH
const diff = `------- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("text with replaced\nend")
@@ -112,18 +112,18 @@ replaced
})
it("should handle special regex chars and nested search markers", async () => {
const original = `text with $^.*\n<<< SEARCH\nend`
const diff = `<<<<<<< SEARCH
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `------- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
<<< SEARCH
------- SEARCH
--- SEARCH
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("text with replaced\nbefore\nend")
@@ -131,18 +131,18 @@ before
})
it("cnfc2 should handle invalid search marker format", async () => {
const original = `text with $^.*\n<<< SEARCH\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
<<< SEARCH
------- SEARCH
--- SEARCH
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
try {
await cnfc(diff, original, true)
expect.fail("Expected an error to be thrown")
@@ -154,18 +154,18 @@ before
})
it("cnfc2 should throw error for incomplete search marker", async () => {
const original = `text with $^.*\n<<< SEARCH\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
<<< SEARCH
------ SEARCH
--- SEARCH
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
try {
@@ -177,18 +177,18 @@ before
})
it("cnfc2 should handle custom nested search markers", async () => {
const original = `text with $^.*\n<<< SEARCH2\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\n--- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
<<< SEARCH2
------ SEARCH
--- SEARCH2
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
@@ -196,18 +196,18 @@ before
})
it("cnfc2 should handle text containing nested search markers", async () => {
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
text with <<< SEARCH2
------ SEARCH
text with --- SEARCH2
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
@@ -215,15 +215,15 @@ before
})
it("cnfc2 should handle missing replacement marker in lenient mode", async () => {
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
text with <<< SEARCH2
------ SEARCH
text with --- SEARCH2
=======
before`
const result1 = await cnfc(diff, original, false)
@@ -233,15 +233,15 @@ before`
})
it("cnfc2 should throw error for missing replacement marker in strict mode", async () => {
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
const diff = `<<< SEARCH
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
text with <<< SEARCH2
------ SEARCH
text with --- SEARCH2
=======
before`
const result1 = await cnfc(diff, original, true)
@@ -262,23 +262,23 @@ Section 3: sed do eiusmod tempor
Section 4: incididunt ut labore
Section 5: et dolore magna aliqua`
const diff = `<<< SEARCH
const diff = `--- SEARCH
Section 1: Lorem ipsum dolor sit amet
=======
Section 1: Replaced text
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
------- SEARCH
Section 3: sed do eiusmod tempor
=======
Section 3: Modified content
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<<< SEARCH
------- SEARCH
Section 5: et dolore magna aliqua
=======
Section 5: Final replacement
>>>>>>> REPLACE`
+++++++ REPLACE`
const expected = `This is a long text with multiple sections.
Section 1: Replaced text
@@ -293,17 +293,17 @@ Section 5: Final replacement
})
// Test diff containing special regex characters and nested search markers
const diff = `<<< SEARCH
const diff = `--- SEARCH
$^.*
=======
replaced
>>>>>>> REPLACE
+++++++ REPLACE
<<<<<< SEARCH
<<< SEARCH
------ SEARCH
--- SEARCH
=======
before
>>>>>>> REPLACE`
+++++++ REPLACE`
// expected1 shows the incremental results when processing the diff line by line
// Each element represents the result after processing that line number
const expected1 = [
@@ -335,7 +335,7 @@ before
const diffLines = diff.split("\n")
for (let i = 1; i < diffLines.length; i++) {
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
const original = `text with $^.*\n<<< SEARCH\nend`
const original = `text with $^.*\n--- SEARCH\nend`
const result1 = await cnfc(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
expect(result1).to.equal(expected1[i - 1])
})
@@ -343,7 +343,7 @@ before
for (let i = 1; i < diffLines.length; i++) {
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
const original = `text with $^.*\n<<< SEARCH\nend`
const original = `text with $^.*\n--- SEARCH\nend`
let expected = expected2[i - 1]
if (expected instanceof Error) {
try {
+11 -1
View File
@@ -3,6 +3,8 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import * as path from "path"
import { UriServiceClient } from "../../../standalone/services/host-grpc-client"
import { Metadata, StringRequest } from "@shared/proto/common"
/**
* Converts a list of URIs to workspace-relative paths
@@ -17,7 +19,15 @@ export const getRelativePaths: FileMethodHandler = async (
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
const fileUri = vscode.Uri.parse(uriString, true)
// Use the host URI service client instead of directly using vscode.Uri.parse
const parseResponse = await UriServiceClient.parse(
StringRequest.create({
metadata: Metadata.create({}),
value: uriString,
}),
)
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
// If the path is still absolute, it's outside the workspace
@@ -0,0 +1,19 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { FileMethodHandler } from "./index"
import path from "path"
/**
* Opens a file in the editor
* @param controller The controller instance
* @param request The request message containing the file path in the 'value' field
* @returns Empty response
*/
export const openTaskHistory: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<Empty> => {
const globalStoragePath = controller.context.globalStorageUri.fsPath
const taskHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
if (request.value) {
openFileIntegration(taskHistoryPath)
}
return Empty.create()
}
@@ -0,0 +1,58 @@
import { Controller } from "../index"
import { EmptyRequest, StringArray } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeWorkspaceUpdateSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to workspace file updates
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToWorkspaceUpdates(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeWorkspaceUpdateSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeWorkspaceUpdateSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "workspace_update_subscription" }, responseStream)
}
}
/**
* Send a workspace update event to all active subscribers
* @param filePaths Array of file paths to send
*/
export async function sendWorkspaceUpdateEvent(filePaths: string[]): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeWorkspaceUpdateSubscriptions).map(async (responseStream) => {
try {
const event = StringArray.create({
values: filePaths,
})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending workspace update event:", error)
// Remove the subscription if there was an error
activeWorkspaceUpdateSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+16 -160
View File
@@ -1,23 +1,17 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { handleModelsServiceRequest } from "./models"
import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { BrowserSession } from "@services/browser/BrowserSession"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
@@ -25,38 +19,29 @@ import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { getTotalTasksSize } from "@utils/storage"
import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
ensureWorkflowsDirectoryExists,
} from "../storage/disk"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task, cwd } from "../task"
import { Task } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -65,6 +50,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
*/
export class Controller {
readonly id: string = uuidv4()
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
@@ -82,7 +68,7 @@ export class Controller {
this.outputChannel.appendLine("ClineProvider instantiated")
this.postMessage = postMessage
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.workspaceTracker = new WorkspaceTracker()
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
@@ -148,6 +134,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
enableCheckpointsSetting,
isNewUser,
taskHistory,
@@ -182,6 +169,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
enableCheckpointsSetting ?? true,
customInstructions,
task,
@@ -215,64 +203,6 @@ export class Controller {
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({
type: "theme",
text: JSON.stringify(theme),
}),
)
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
this.postMessageToWebview({
type: "openRouterModels",
openRouterModels,
})
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
handleModelsServiceRequest(this, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
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 } = await getAllExtensionState(this.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
this.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
}
})
break
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
// Send a message to our webview.
// You can send any JSON serializable data.
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images, message.files)
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -355,48 +285,6 @@ export class Controller {
await this.postStateToWebview()
break
}
case "updateSettings": {
// api config
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
if (this.task) {
this.task.api = buildApiHandler(message.apiConfiguration)
}
}
// custom instructions
await this.updateCustomInstructions(message.customInstructionsSetting)
// telemetry setting
if (message.telemetrySetting) {
await this.updateTelemetrySetting(message.telemetrySetting)
}
// plan act setting
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
if (typeof message.enableCheckpointsSetting === "boolean") {
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
}
if (typeof message.mcpMarketplaceEnabled === "boolean") {
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
}
// chat settings (including preferredLanguage and openAIReasoningEffort)
if (message.chatSettings) {
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
if (this.task) {
this.task.chatSettings = message.chatSettings
}
}
// after settings are updated, post state to webview
await this.postStateToWebview()
await this.postMessageToWebview({ type: "didUpdateSettings" })
break
}
case "clearAllTaskHistory": {
const answer = await vscode.window.showWarningMessage(
"What would you like to delete?",
@@ -428,13 +316,6 @@ export class Controller {
}
break
}
case "executeQuickWin":
if (message.payload) {
const { command, title } = message.payload
this.outputChannel.appendLine(`Received executeQuickWin: command='${command}', title='${title}'`)
await this.initTask(title)
}
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
@@ -738,10 +619,6 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
@@ -787,10 +664,7 @@ export class Controller {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -818,27 +692,17 @@ export class Controller {
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
@@ -1018,18 +882,6 @@ export class Controller {
throw new Error("Task not found")
}
async showTaskWithId(id: string) {
if (id !== this.task?.taskId) {
// non-current task
const { historyItem } = await this.getTaskWithId(id)
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
}
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
}
async exportTaskWithId(id: string) {
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
await downloadTask(historyItem.ts, apiConversationHistory)
@@ -1173,7 +1025,9 @@ export class Controller {
globalClineRulesToggles,
globalWorkflowToggles,
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1217,7 +1071,9 @@ export class Controller {
localWorkflowToggles: localWorkflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
}
}
+2 -4
View File
@@ -3,6 +3,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
import { McpServer, McpDownloadResponse } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Download an MCP server from the marketplace
@@ -77,10 +78,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// Initialize task and show chat view
await controller.initTask(task)
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
// Return an empty response - the client only cares if the call succeeded
return Empty.create()
@@ -0,0 +1,55 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { McpMarketplaceCatalog } from "@shared/proto/mcp"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeMcpMarketplaceSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to MCP marketplace catalog updates
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToMcpMarketplaceCatalog(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeMcpMarketplaceSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream)
}
}
/**
* Send an MCP marketplace catalog event to all active subscribers
*/
export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => {
try {
await responseStream(
catalog,
false, // Not the last message
)
} catch (error) {
console.error("Error sending MCP marketplace catalog event:", error)
// Remove the subscription if there was an error
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -18,7 +18,7 @@ export async function getOllamaModels(controller: Controller, request: StringReq
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set<string>(modelsArray)]
const models = [...new Set<string>(modelsArray)].sort()
return StringArray.create({ values: models })
} catch (error) {
@@ -15,11 +15,11 @@ import { GlobalFileNames } from "@core/storage/disk"
*/
export async function refreshOpenRouterModels(
controller: Controller,
request: EmptyRequest,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
let models: Record<string, OpenRouterModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -32,15 +32,20 @@ export async function refreshOpenRouterModels(
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
const modelInfo = OpenRouterModelInfo.create({
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
inputPrice: parsePrice(rawModel.pricing?.prompt) ?? 0,
outputPrice: parsePrice(rawModel.pricing?.completion) ?? 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: rawModel.description ?? "",
thinkingConfig: rawModel.thinking_config ?? undefined,
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
tiers: rawModel.tiers ?? [],
})
switch (rawModel.id) {
case "anthropic/claude-sonnet-4":
@@ -118,7 +123,7 @@ export async function refreshOpenRouterModels(
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", models)
console.log("OpenRouter models fetched and saved", JSON.stringify(models).slice(0, 300))
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
@@ -129,30 +134,13 @@ export async function refreshOpenRouterModels(
}
}
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
return OpenRouterCompatibleModelInfo.create({ models })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
@@ -0,0 +1,60 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active OpenRouter models subscriptions
const activeOpenRouterModelsSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to OpenRouter models events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToOpenRouterModels(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
console.log("[DEBUG] set up OpenRouter models subscription")
// Add this subscription to the active subscriptions
activeOpenRouterModelsSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeOpenRouterModelsSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up OpenRouter models subscription")
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "openRouterModels_subscription" }, responseStream)
}
}
/**
* Send an OpenRouter models event to all active subscribers
* @param models The OpenRouter models to send
*/
export async function sendOpenRouterModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeOpenRouterModelsSubscriptions).map(async (responseStream) => {
try {
await responseStream(
models,
false, // Not the last message
)
console.log("[DEBUG] sending OpenRouter models event")
} catch (error) {
console.error("Error sending OpenRouter models event:", error)
// Remove the subscription if there was an error
activeOpenRouterModelsSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+2 -4
View File
@@ -2,6 +2,7 @@ import { Controller } from ".."
import { Empty, EmptyRequest } from "../../../shared/proto/common"
import { resetExtensionState } from "../../../core/storage/state"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Resets the extension state to its defaults
@@ -22,10 +23,7 @@ export async function resetState(controller: Controller, request: EmptyRequest):
vscode.window.showInformationMessage("State reset")
await controller.postStateToWebview()
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
return Empty.create()
} catch (error) {
@@ -0,0 +1,85 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { UpdateSettingsRequest } from "../../../shared/proto/state"
import { updateApiConfiguration } from "../../storage/state"
import { buildApiHandler } from "../../../api"
import { convertProtoApiConfigurationToApiConfiguration } from "../../../shared/proto-conversions/state/settings-conversion"
import { convertProtoChatSettingsToChatSettings } from "../../../shared/proto-conversions/state/chat-settings-conversion"
import { TelemetrySetting } from "@/shared/TelemetrySetting"
/**
* Updates multiple extension settings in a single request
* @param controller The controller instance
* @param request The request containing the settings to update
* @returns An empty response
*/
export async function updateSettings(controller: Controller, request: UpdateSettingsRequest): Promise<Empty> {
try {
// Update API configuration
if (request.apiConfiguration) {
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
await updateApiConfiguration(controller.context, apiConfiguration)
if (controller.task) {
controller.task.api = buildApiHandler(apiConfiguration)
}
}
// Update custom instructions
if (request.customInstructionsSetting !== undefined) {
await controller.updateCustomInstructions(request.customInstructionsSetting)
}
// Update telemetry setting
if (request.telemetrySetting) {
await controller.updateTelemetrySetting(request.telemetrySetting as TelemetrySetting)
}
// Update plan/act separate models setting
if (request.planActSeparateModelsSetting !== undefined) {
await controller.context.globalState.update("planActSeparateModelsSetting", request.planActSeparateModelsSetting)
}
// Update checkpoints setting
if (request.enableCheckpointsSetting !== undefined) {
await controller.context.globalState.update("enableCheckpointsSetting", request.enableCheckpointsSetting)
}
// Update MCP marketplace setting
if (request.mcpMarketplaceEnabled !== undefined) {
await controller.context.globalState.update("mcpMarketplaceEnabled", request.mcpMarketplaceEnabled)
}
// Update MCP responses collapsed setting
if (request.mcpResponsesCollapsed !== undefined) {
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
}
// Update chat settings
if (request.chatSettings) {
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
await controller.context.globalState.update("chatSettings", chatSettings)
if (controller.task) {
controller.task.chatSettings = chatSettings
}
}
// Update terminal timeout setting
if (request.shellIntegrationTimeout !== undefined) {
await controller.context.globalState.update("shellIntegrationTimeout", Number(request.shellIntegrationTimeout))
}
// Update terminal reuse setting
if (request.terminalReuseEnabled !== undefined) {
await controller.context.globalState.update("terminalReuseEnabled", request.terminalReuseEnabled)
}
// Post updated state to webview
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error("Failed to update settings:", error)
throw error
}
}
@@ -0,0 +1,35 @@
import { ExecuteQuickWinRequest } from "@shared/proto/task"
import { Empty } from "@shared/proto/common"
import type { Controller } from "../index"
/**
* Executes a quick win task with command and title
* @param controller The controller instance
* @param request The execute quick win request
* @returns Empty response
*
* @example
* // Usage from webview:
* import { TaskServiceClient } from "@/services/grpc-client"
* import { ExecuteQuickWinRequest } from "@shared/proto/task"
*
* const request: ExecuteQuickWinRequest = {
* command: "npm install",
* title: "Install dependencies"
* }
*
* TaskServiceClient.executeQuickWin(request)
* .then(() => console.log("Quick win executed successfully"))
* .catch(error => console.error("Failed to execute quick win:", error))
*/
export async function executeQuickWin(controller: Controller, request: ExecuteQuickWinRequest): Promise<Empty> {
try {
const { command, title } = request
console.log(`Received executeQuickWin: command='${command}', title='${title}'`)
await controller.initTask(title)
return Empty.create({})
} catch (error) {
console.error("Failed to execute quick win:", error)
throw error
}
}

Some files were not shown because too many files have changed in this diff Show More