Compare commits

...

102 Commits

Author SHA1 Message Date
Andrei Eternal ff9f208a30 a better pattern for a callback based bridge without using the requestRegistry directly 2025-06-02 19:49:03 -07:00
Andrei Eternal 8d3bbf32a0 fix capitalization of uri.proto 2025-06-02 19:10:07 -07:00
Andrei Eternal e5eaf89761 Revert "remove registry use todo"
This reverts commit 84078d3469.
2025-06-02 18:53:21 -07:00
Andrei Eternal 84078d3469 remove registry use todo 2025-06-02 18:27:31 -07:00
Andrei Eternal 0a2fff3c97 remove some todos and logs 2025-06-02 16:48:56 -07:00
Andrei Eternal 84cdd9331f remove test setup 2025-06-02 16:46:27 -07:00
Andrei Eternal c7d3eab558 debounce new file watcher 2025-06-02 16:43:03 -07:00
Andrei Eternal 4a951e1873 use StringRequest for uri.proto 2025-06-02 15:17:54 -07:00
Andrei Eternal 59f526b36f Merge remote-tracking branch 'origin/main' into protobus-host-bridge 2025-06-02 14:35:55 -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
Andrei Eternal f33ae894a0 another todo 2025-06-02 11:45:44 -07:00
Andrei Eternal 7f4026b75d add a method for canceling request rather than using the registry 2025-06-02 11:36:58 -07:00
Andrei Eternal 30361d0f7b remove unneeded functions from host-grpc-handler 2025-06-02 11:21:00 -07:00
Andrei Eternal 329fd25a5a add cancellation for host grpc stream 2025-06-02 11:11:47 -07:00
Andrei Eternal cb0b84a572 format, comment 2025-06-02 10:55:15 -07:00
Andrei Eternal fb75848db7 first wip subscription host bridge demo for watching mcp server config 2025-06-01 21:59:09 -07:00
Andrei Eternal 53dadbe115 fix streaming mode for host bridge 2025-06-01 20:37:16 -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
Andrei Eternal 21bf659405 get host responses correctly 2025-06-01 19:22:28 -07:00
Andrei Eternal 7cb14a388c remove adapter pattern 2025-06-01 19:12:30 -07:00
Andrei Eternal 174d368f6d host handler to make other hosts easier to add 2025-06-01 19:00:30 -07:00
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
Evan 97c25cb35e Pass id to webview on creation (#3867)
* pass type of webview to webview

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-01 14:06:48 -07:00
suntp 0fb82c1975 fix:The POST requests of MCP's SSE server support setting headers. (#2969)
* fix:The POST requests of MCP's SSE server support setting headers.(#2652)

* fix:The POST requests of MCP's SSE server support setting headers.(#2652)

---------

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>
2025-06-01 13:25:31 -07:00
pashpashpash 36c3f93884 added modelId to tool call events (#3968) 2025-06-01 13:24:12 -07:00
pashpashpash 3fb0360e01 fixing ripgrep overload - tool should not return more than 0.25mb max… (#3967)
* fixing ripgrep overload - tool should not return more than 0.25mb max, but it can easily return more than 9mb in some cases

* changeset
2025-06-01 13:24:00 -07:00
Ara 084c0a73a3 Apply new edit tool to diff (#3944)
* add ls file tool description, parsing, and return formatting

* add json tool definition and remove extra '.'

* changeset

* use separate function for new format

* using json

* add grep tool new format

* add editTool definition

* Map MultiEdit tool to StreamingJsonReplacer with logs

* Adding support for non streamed json

* Adding logging

* moving multiedit tool into tool defs

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: pashpashpash <nik@nugbase.com>
2025-06-01 13:23:52 -07:00
Tomás Barreiro 092bd17921 feat: Add delay information when retrying requests (#3817)
* Add delay information when retrying requests

* Display delays

* refactor and add countdown
2025-06-01 12:23:47 -07:00
Ara 079d05c2cc Fixing OpenAI compatible to support cache token display (#3957)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-01 05:55:31 +05:30
Ara acc795ed69 Fix pricing and token counting for Xai Provider for the new Grok 3 family of models (#3956) 2025-05-31 11:49:29 -07:00
Elon Gliksberg 1aaa30ecce Closing MCP processes when app terminates. (#3876) 2025-05-31 01:44:03 -07:00
Sarah Fortune f7e5398ac8 Fix warnings for protobuf object literals (#3948)
Create protobuf objects with .create()
2025-05-31 01:35:29 -07:00
Gustavo A. Rodríguez Suárez 89f35b0800 Fix undefined type during chunk streaming (#1464)
* Fix undefined type when parsing response chunk in stream

* Fix undefined type when parsing response chunk in stream

* remove Cline.ts changes

---------

Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-05-31 00:02:28 -07:00
Sarah Fortune d7f30fdf73 Make the no-grpc-client-object-literals linter rule an error for the webview-ui (#3947)
* Fix linter warnings in the webview (part 2)

Replace protobus calls using object literals to use Message.create({...})

Fix incorrect property name detected after this change in webview-ui/src/components/settings/SettingsView.tsx

Optimised imports in vscode.

* formatting

* 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

* Only include webview grpc ServiceClient check

* Fix lint errors

* formatting

* Update package.json

* Make the no-grpc-client-object-literals linter rule an error for the webview-ui

Fix the last occurrence of this issue.

* formatting
2025-05-30 20:31:14 -07:00
Sarah Fortune 0c6a4f9452 feat(lint): Add custom ESLint rules for protobuf type checking for protobus ServiceClients (#3946)
* Fix linter warnings in the webview (part 2)

Replace protobus calls using object literals to use Message.create({...})

Fix incorrect property name detected after this change in webview-ui/src/components/settings/SettingsView.tsx

Optimised imports in vscode.

* formatting

* 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

* Only include webview grpc ServiceClient check

* Fix lint errors

* formatting

* Update package-lock.json

* Update package.json
2025-05-30 20:19:04 -07:00
Toshii c1e38e649c file selection proto input type change (#3945)
* request type

* changeset
2025-05-30 20:09:41 -07:00
Sarah Fortune b8a65a446a Fix linter warnings in the webview (part 2) (#3943)
* Fix linter warnings in the webview (part 2)

Replace protobus calls using object literals to use Message.create({...})

Fix incorrect property name detected after this change in webview-ui/src/components/settings/SettingsView.tsx

Optimised imports in vscode.

* Fix typo

* formatting
2025-05-30 19:37:27 -07:00
Daniel Steigman 6626124bef fix(bedrock): resolve AWS credential caching issue with Identity Manager (#3936)
* fix(bedrock): resolve AWS credential caching issue with Identity Manager

- Add ignoreCache option for profile-based authentication to detect external credential file changes
- Implement smart caching for manual credentials with 5-minute TTL to maintain performance
- Add configuration hash-based cache invalidation for manual credential changes
- Add invalidateCredentialCache() method for error recovery scenarios

Fixes issue where AWS Identity Manager credential updates were not detected,
requiring extension restart. Profile-based authentication now always reads
fresh credentials while manual credentials maintain performance through caching.

Resolves credential refresh issues reported by users using AWS Identity Manager
with role-based authentication workflows.

* Potential fix for code scanning alert no. 66: Use of a broken or weak cryptographic algorithm

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* merge conflict

* updated to fixe the original medrock issue

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-05-30 19:11:20 -07:00
Sarah Fortune 80f67c3c89 Fix linter warning for Protobus ServiceClient calls using object literals. (#3942)
Fix linter warnings in the first half of the webview.
2025-05-30 18:53:01 -07:00
Sarah Fortune 01afe5ec53 Use the same version of eslint for the webview as for the cline package. (#3940)
The webview and the cline package were using different version of eslint,
which makes it difficult to use custom rules because the webview version wants the rules as
ES modules, but the cline version wants commonJS modules.

Switch the webview to use the same version as the cline package.

Switch the webview eslint JS config to the json config file.
2025-05-30 18:43:34 -07:00
Andrei Eternal 627464ea48 better layout 2025-05-30 16:59:22 -07:00
Evan 83928ccecc Add edit tool definition (#3939)
* add ls file tool description, parsing, and return formatting

* add json tool definition and remove extra '.'

* changeset

* use separate function for new format

* using json

* add grep tool new format

* add editTool definition

* removed changeset

* removed changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-30 16:10:33 -07:00
Sarah Fortune 38a1179e62 Rename SecretStore.storeSecret to SecretStore.store (#3897)
Apparently ChatGPT can't follow the SDK docs.
2025-05-30 15:45:17 -07:00
Sarah Fortune e0f9eeaa79 Use the webview-ui linter during npm run lint (#3938)
The webview-ui has an existing eslint config, but it was not being run as part of `npm run lint` command. Start running the webview specific linter (the top level linter doesn't run on tsx files, and webview linter has react specific checks).
Fix lint errors in slash-commands file.
2025-05-30 15:21:34 -07:00
Andrei Eternal f10a82916f Warn about rosetta on osx in build-protos (#3400)
* warn about rosetta on osx in build-protos

* make one console log better

* format

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-05-30 15:05:07 -07:00
Tomás Barreiro da12437251 fix: update the model list when the client requests a refresh (#3882)
* update the model list when the client refreshes

* Refactor and use the grpc response instead of a webview message
2025-05-30 14:28:58 -07:00
Donovan Sydow fb3012f778 add llama4 models, and mis/codestral models to vertex api (#3474)
* add llama4 models, and mis/codestral models to vertex api

* update mistral context window sizes

* changeset

---------

Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-05-30 14:23:54 -07:00
pashpashpash 8336831d8f read tool + write tool + webfetch tool + question tool + usemcp tool + list code definition names tool + access MCP resource tool + load mcp documentation tool + attempt completion tool + browser tool + new task tool (#3925) 2025-05-30 13:16:37 -07:00
Andrei Eternal f0c357f804 gitignore more files 2025-05-30 11:52:22 -07:00
Andrei Eternal 2ebc56a9ed remove tmp impl & rename host grpc client 2025-05-30 11:49:47 -07:00
KevinTurnbull ec064426b3 Minimal change to respect Ollama Context Window Size (#3880)
* Respect the CtxNum setting for Ollama Models

Currently since the context window size isn't respected for Ollama models - the LLM does a naive truncation which removes important details. This leads to the model entering endless loops or making unsupported edits when operating as an agent.

* small change

* changeset

---------

Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-05-30 01:45:47 -07:00
canvrno 8f72bf11f7 optionsResponse protobus migration/removal (#3860) 2025-05-29 22:47:22 -07:00
pashpashpash 915cf76f85 Pashpashpash/bash tool (#3894)
* bashTool

* prettier

* alignment

* bashtool cont

* bash tool cont

* modularizing prompt a bit

* bash tool working

* bash tool now getting cwd

* bashTool

* forgot .name

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-29 20:09:07 -07:00
Toshii f1fef24f25 support xlsx and csv (#3922) 2025-05-29 18:41:21 -07:00
Peter Dave Hello ceb0900bf6 Update xaiModels and xaiDefaultModelId in src/shared/api.ts (#3814) 2025-05-29 18:29:30 -07:00
Toshii cc9fc9bd1f update chat box ui (#3868)
* chat area

* changeset

* arrow

* nit

* tailwind
2025-05-29 13:23:59 -07:00
Ara dcf59bc29f Fix Title for Cline on Windows (#3917)
* Fix Title for Cline on Windows

* Fix Title for Cline on Windows

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-30 01:04:54 +05:30
Caleb Eom 5c3e7a38d4 Scroll to message onclick from task timeline (#3890)
* Scroll to message onclick from task timeline

* version

* fixing ellipsis-dev's suggestion on potential infinite loop
2025-05-29 00:49:08 -07:00
github-actions[bot] d6ccbcdf22 v3.17.8 Release Notes
v3.17.8 Release Notes
2025-05-28 23:01:23 -07:00
pashpashpash e4e03fa0dd reverting timeout if no first chunk (#3899)
* reverting timeout if no first chunk

* Create fresh-days-end.md

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-05-28 21:04:48 -07:00
pashpashpash 10892670be exact antml (#3891)
* exact antml

* only one invoke

* linter warnings

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-28 19:12:26 -07:00
Evan 3c7a42c35d Add grep tool new format (#3893)
* add ls file tool description, parsing, and return formatting

* add json tool definition and remove extra '.'

* changeset

* use separate function for new format

* using json

* add grep tool new format

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-28 18:32:18 -07:00
Evan abfaf6ca7a LS is moar (#3883)
* add ls file tool description, parsing, and return formatting

* add json tool definition and remove extra '.'

* changeset

* use separate function for new format

* using json

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-28 16:15:38 -07:00
pashpashpash f699f1fd80 exact antml (#3887)
* exact antml

* moved system prompt after tool defs

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-28 16:07:17 -07:00
Sarah Fortune a6fbdcb5c1 Add secrets to the vscode extension context. (#3881)
* Add secrets to the vscode extension context.

Add a secrets store backed by a file.
Compile the standalone distribtion during `npm run pretest`

* Fix type

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

* Remove logging

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-28 15:02:21 -07:00
Sarah Fortune a4200869c9 Add java_package to state.proto (#3886) 2025-05-28 15:01:21 -07:00
canvrno 9f59b6010e browserConnectionResult removal (#3885) 2025-05-28 14:48:07 -07:00
canvrno 29d3175e0b Add ReadTool & WriteTool alternate tools (#3873)
* Added ReadTool and WriteTool alternate tool calls

* claude4 system prompt - add read & write alt tools
2025-05-28 14:08:46 -07:00
Sarah Fortune 0012de9212 Run formatter 2025-05-28 00:11:25 -07:00
pashpashpash 600174322e created framework for idiomatic tool calling in claude 4 models (#3872)
* created framework for idiomatic tool calling in claude 4 models

* aligning

* Update src/core/tools/bashTool.ts

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

* Fix Diff edit prompt

* commented out for now

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-27 23:39:33 -07:00
pashpashpash 9818d976dd Canvrno/modular system prompt (#3863)
* System prompt refactor

* Commented out model switching for now

* Claude4 system prompt switching

* cleanup

* added system.ts to prettier ignore

* workflow tips

---------

Co-authored-by: canvrno <kevin@cline.bot>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-27 22:42:58 -07:00
Andrei Eternal 3938e23cde [PROTOBUS] subscription for addToInput (#3781)
* Protobus subscription for addToInput

* formatfix

* prettier

* dont re-add selectedImages

---------

Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-05-27 21:43:14 -07:00
github-actions[bot] 86bb0c6ded Changeset version bump (#3864)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.17.7

---------

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: Cline Evaluation <cline@example.com>
2025-05-27 20:15:08 -07:00
Ara 36f57ce6c0 Fix Claude 4 family diff edit prompt (#3866)
* Fix Claude 4 diff edits prompt

* Show gemini 2.5 flash prompt cache

* Update src/core/prompts/system.ts

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

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-27 20:02:11 -07:00
canvrno ed0181a114 showChatView protobus (#3862) 2025-05-27 18:19:51 -07:00
github-actions[bot] 439c62935d v3.17.6 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.17.6

* attribution

---------

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: Cline Evaluation <cline@example.com>
2025-05-27 17:33:54 -07:00
Sean Gallen 9bc24ecd96 Minor fixes in the Documentation for broken links and typos. (#3820)
* fix bash commands

* fix links in the Context Management

* fix for internal links in Our Favorite Tech Stack
2025-05-27 17:17:34 -07:00
francis 2150e4882e feat: add vscode language model api config docs (#3836) 2025-05-27 17:15:04 -07:00
Toshii ee347bfe9d allow uploading more file types (#3824)
* Changeset version bump (#3740)

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.17.1

---------

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: Cline Evaluation <cline@example.com>

* process files

* grpc

* select files

* update messaging

* chat view support

* thumbnails base

* ui component

* more files

* pre file parsing

* keep old file

* base 3

* file passing update

* ui fixes

* task base

* header ui

* grpc

* changeset

* nit

* remove binary check

* small

---------

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: Cline Evaluation <cline@example.com>
2025-05-27 17:13:15 -07:00
kevinneung 08d86990e2 Improve documentation for "For New Coders" page (#3785)
* Improve documentation for new coders

- Reorganized steps for better flow and clarity
- Added missing hyperlinks for better navigation
- Underlined hyperlinks for improved accessibility and visual consistency

* Update docs/getting-started/for-new-coders.mdx

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-27 16:52:27 -07:00
Ara f74a8ba82e Show Gemini 2.5 Pro prompt cache (#3859)
* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-28 04:42:41 +05:30
Evan ad51b7a4e1 reset recommended model (#3857)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-27 16:10:06 -07:00
Ara 94516092b1 Support Diff Editing for Claude 4 family of Models (#3816)
* feat: add JSON-based diff format for Claude 4 model family

- Bump version to 3.17.5
- Add @streamparser/json dependency for streaming JSON parsing
- Implement new JSON diff format in replace_in_file tool for Claude 4 models
- Add diff-json.ts module for handling JSON-based file replacements
- Update system prompts to use JSON format when Claude 4 model detected
- Enhance DiffViewProvider to support new JSON diff format

* Adding diffs

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-27 15:10:48 -07:00
Ara adf25681cc Add a beautiful experience for new users of Cline (#3719)
* Adding AGI Blog

* feat: disable quick wins feature in chat interface

Removes quick wins display by setting shouldShowQuickWins to false, cleans up related code in ChatView and simplifies component rendering logic. Also includes code cleanup in QuickWinCard component by removing redundant comments.

* Initial edit commands

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-27 14:34:43 -07:00
canvrno e2f73bce61 [PROTOBUS] remove invoke message, replace usage (#3609)
* invoke protobus migration

Updated Cline API invoke usage for GRPC

cleanup

* cleanup

* Updated ClineAPI

* cleanup

* Added ClineAPI tests

* removed invoke

* One line cleanup
2025-05-27 13:59:52 -07:00
Evan fec8626291 Migrate authCallback protobus (#3846)
* migrate authCallback

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-27 08:57:05 -07:00
Kevin Taylor 6fa819a170 Added Cerebras as a Provider (#3810)
* Added Cerebras as a Provider

* prettier fix

* prettier

---------

Co-authored-by: sam <sam@MacBook-Air-3.local>
2025-05-26 20:06:15 -07:00
Andrei Eternal 2ca3e9ac82 [PROTOBUS] Re-enable streaming state, fix the memory leak probably (#3754)
* Revert "fix: Address memory leak by bypassing subscribeToState gRPC stream for state updates (#3597)"

This reverts commit 8ab35a5b06.

* memory leak console boys

* cleanup

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-05-26 18:17:09 -07:00
Luis Felipe Salazar Ucros 9d801a1a68 Sambanova models update list and docs link (#3419)
* update models list

* update docs link with utm

* remove tracking link
2025-05-26 15:50:59 -07:00
Trevor Hudson 7bfc00b80e add missing fuction (#3834) 2025-05-26 15:36:02 -07:00
canvrno a346f05e9c [PROTOBUS] Move requestTotalTasksSize to protobus (#3608)
* requestTotalTasksSize protobus migration

* Fix EmpyRequest value error

* removed hook

* Removed task size refresh actions from backend
2025-05-26 14:18:53 -07:00
Trevor Hudson 35929b6869 Use identify to enhance distinct user segmentation (#3765)
include backup id in front end

variables clarity
2025-05-26 12:12:24 -07:00
canvrno ffeee7e48d [PROTOBUS] Move openInBrowser to protobus (#3691)
* openInBrowser_protobus_migration
2025-05-26 11:20:37 -07:00
Evan 10239f0616 Migrate showAccountViewClicked protobus (#3787)
* Stop tracking auto-generated files, respect .gitignore

* migrate showAccountViewClicked

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-23 18:21:19 -07:00
Evan 9f1b01b561 Migrate openExtensionSettings protobus (#3786)
* Stop tracking auto-generated files, respect .gitignore

* migrate openExtensionSettings

* changeset

* remove comment

* Stop tracking auto-generated files, respect .gitignore

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-23 18:20:55 -07:00
Ara 7f641072d4 Adds telemetry to record the usage of keyboard and lightbulb icon shortcuts in Cline (#3695)
* Fix: Temporary revert protobus changes for Toggle plan and act mode

* Adding telemetry

* Adding telemetry

* Adding telemetry

* Adding telemetry

* Adding telemetry

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-24 05:02:12 +05:30
Evan cf0af8a3f0 Migrate openMcpSettings protobus (#3778)
* migrate openMcpSettings

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-23 14:46:53 -07:00
github-actions[bot] b9551c960a v3.17.5 Release Notes (#3777)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-05-23 09:50:58 -07:00
Saoud Rizwan f2ffe26aaf fix: add instruction to use valid SEARCH/REPLACE markers when diff edit fails (#3776)
* fix: add instruction to use valid SEARCH/REPLACE markers when diff edit fails

* Create proud-trains-give.md
2025-05-23 09:46:04 -07:00
Andrei Edell 300402c8ba WIP host bridge 2025-05-22 12:54:58 -07:00
244 changed files with 14161 additions and 13145 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
add models to vertex ai
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Update `xaiModels` object and `xaiDefaultModelId` in `src/shared/api.ts`
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fixing token counting for xai provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate mcpButtonClicked to protobus
+7
View File
@@ -0,0 +1,7 @@
---
"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
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add suppport for parsing csv and xlsx
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add dev only button to open task conversation history
+5
View File
@@ -0,0 +1,5 @@
---
"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
---
add model to nebius ai studio
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
update chat box ui
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
bug fix for ollama
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
update the openrouter model list when refreshing
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix Title for windows in cline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
scroll to task timeline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add grep tool with new parsing format
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
optionsResponse protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Display delay information when retrying API calls
+5
View File
@@ -0,0 +1,5 @@
---
"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
---
Add edit tool definition
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
change proto type
+1
View File
@@ -164,6 +164,7 @@ Key providers include:
- **OpenRouter**: Meta-provider supporting multiple model providers
- **AWS Bedrock**: Integration with Amazon's AI services
- **Gemini**: Google's AI models
- **Cerebras**: High-performance inference with Llama, Qwen, and DeepSeek models
- **Ollama**: Local model hosting
- **LM Studio**: Local model hosting
- **VSCode LM**: VSCode's built-in language models
+3 -2
View File
@@ -5,7 +5,7 @@
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"plugins": ["@typescript-eslint", "eslint-rules"],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
@@ -19,7 +19,8 @@
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off",
"react-hooks/exhaustive-deps": "off"
"react-hooks/exhaustive-deps": "off",
"eslint-rules/no-grpc-client-object-literals": "error"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
}
+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
+2
View File
@@ -3,3 +3,5 @@ node_modules
webview-ui/build/
*.md
package-lock.json
src/core/prompts/system.ts
src/core/prompts/model_prompts/claude4.ts
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## [3.17.8]
- Fix bug where terminal would get stuck and output "capture failure"
## [3.17.7]
- Fix diff editing reliability for Claude 4 family models by adding constraints to prevent errors with large replacements
## [3.17.6]
- Add Cerebras as a new API provider with 5 high-performance models including reasoning-capable models (Thanks @kevint-cerebras!)
- Add support for uploading various file types (XML, JSON, TXT, LOG, MD, DOCX, IPYNB, PDF) alongside images
- Add improved onboarding experience for new users with guided setup
- Add prompt cache indicator for Gemini 2.5 Flash models
- Update SambaNova provider with new model list and documentation links (Thanks @luisfucros!)
- Fix diff editing support for Claude 4 family of models
- Improve telemetry and analytics for better user experience insights
## [3.17.5]
- Fix issue with Claude 4 models where after several conversation turns, it would start making invalid diff edits
## [3.17.4]
- Fix thinking budget slider for Claude 4
+1 -1
View File
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
### Use any API and Model
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
@@ -0,0 +1,44 @@
---
title: "VS Code Language Model API"
description: "Learn how to use Cline with the experimental VS Code Language Model API, enabling access to models from GitHub Copilot and other compatible extensions."
---
Cline offers _experimental_ support for the [VS Code Language Model API](https://code.visualstudio.com/api/extension-guides/language-model). This API enables extensions to grant access to language models directly within the VS Code environment. Consequently, you might be able to leverage models from:
- **GitHub Copilot:** Provided you have an active Copilot subscription and the extension installed.
- **Other VS Code Extensions:** Any extension that implements the Language Model API.
**Important Note:** This integration is currently in an experimental phase and might not perform as anticipated. Its functionality relies on other extensions correctly implementing the VS Code Language Model API.
### Prerequisites
- **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.
### 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`
### Current Limitations
- **Experimental API Status:** The VS Code Language Model API is still under active development. Anticipate potential changes and instability.
- **Dependency on Extensions:** This feature is entirely contingent on other extensions making models available. Cline does not directly control the list of accessible models.
- **Restricted Functionality:** The VS Code Language Model API might not encompass all features available through other API providers (e.g., image input capabilities, streaming responses, detailed usage metrics).
- **No Direct Cost Management:** Users are subject to the pricing structures and terms of service of the extension providing the model. Cline cannot directly monitor or regulate associated costs.
- **GitHub Copilot Rate Throttling:** When employing the VS Code LM API with GitHub Copilot, be mindful that GitHub may enforce rate limits on Copilot usage. These limitations are governed by GitHub, not Cline.
### Troubleshooting Tips
- **Models Not Appearing:**
- Confirm that VS Code is installed.
- Verify that a language model provider extension (e.g., GitHub Copilot, GitHub Copilot Chat) is installed and enabled.
- If utilizing Copilot, ensure you have previously sent a Copilot Chat message using the desired model.
- **Unexpected Operation:** Should you encounter unforeseen behavior, it is likely an issue stemming from the underlying Language Model API or the provider extension. Consider reporting the problem to the developers of the provider extension.
+2 -1
View File
@@ -148,7 +148,8 @@
"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/litellm-and-cline-using-codestral",
"custom-model-configs/vscode-language-model-api"
]
},
{
+27 -27
View File
@@ -13,35 +13,13 @@ Before you jump into coding, make sure you have these essentials ready:
A popular, free, and powerful code editor.
- [Download VS Code](https://code.visualstudio.com/)
- [<u>Download VS Code</u>](https://code.visualstudio.com/)
📺 **Recommended YouTube Tutorial:** [How to Install VS Code](https://www.youtube.com/watch?v=MlIzFUI1QGA)
📺 **Recommended YouTube Tutorial:** [<u>How to Install VS Code</u>](https://www.youtube.com/watch?v=MlIzFUI1QGA)
> ✅ **Pro Tip:** Install VS Code in your Applications folder (macOS) or Program Files (Windows) for easy access from your dock or start menu.
#### 2. **Essential Development Tools**
Basic software required for coding efficiently:
- Homebrew (macOS)
- Node.js
- Git
👉 Follow our detailed guide on Installing Essential Development Tools with step-by-step help from Cline.
📺 **Recommended YouTube Tutorials:**
- **For macOS:**
- [Install Homebrew on Mac](https://www.youtube.com/watch?v=hwGNgVbqasc)
- [Install Git on MacOS 2024](https://www.youtube.com/watch?v=B4qsvQ5IqWk)
- [Install Node.js on Mac (M1 | M2 | M3)](https://www.youtube.com/watch?v=I8H4wolRFBk)
- **For Windows:**
- [Install Git on Windows 10/11 (2024)](https://www.youtube.com/watch?v=yjxv1HuRQy0)
- [Install Node.js in Windows 10/11](https://www.youtube.com/watch?v=uCgAuOYpJd0)
> ⚠️ **Note:** If you run into permission issues during installation, try running your terminal or command prompt as an administrator.
#### 3. **Organize Your Projects**
#### 2. **Organize Your Projects**
Create a dedicated folder named `Cline` in your Documents folder for all your coding projects:
@@ -55,14 +33,36 @@ Inside your `Cline` folder, structure projects clearly:
> 💡 **Tip:** Keeping your projects organized from the start will save you time and confusion later!
#### 4. **Install the Cline VS Code Extension**
#### 3. **Install the Cline VS Code Extension**
Enhance your coding workflow by installing the Cline extension directly within VS Code:
- Get Started with Cline Extension Tutorial
📺 **Recommended YouTube Tutorial:** [How To Install Extensions in VS Code](https://www.youtube.com/watch?v=E7trgwZa-mk)
📺 **Recommended YouTube Tutorial:** [<u>How To Install Extensions in VS Code</u>](https://www.youtube.com/watch?v=E7trgwZa-mk)
> ✅ **Pro Tip:** After installing, reload VS Code to ensure the extension is activated properly.
#### 4. **Essential Development Tools**
Basic software required for coding efficiently:
- Homebrew (macOS)
- Node.js
- Git
👉 [<u>Follow our detailed guide on Installing Essential Development Tools with step-by-step help from Cline.</u>](https://docs.cline.bot/getting-started/installing-dev-essentials#installing-dev-essentials)
📺 **Recommended YouTube Tutorials for Manual Installation:**
- **For macOS:**
- [<u>Install Homebrew on Mac</u>](https://www.youtube.com/watch?v=hwGNgVbqasc)
- [<u>Install Git on macOS 2024</u>](https://www.youtube.com/watch?v=B4qsvQ5IqWk)
- [<u>Install Node.js on Mac (M1 | M2 | M3)</u>](https://www.youtube.com/watch?v=I8H4wolRFBk)
- **For Windows:**
- [<u>Install Git on Windows 10/11 (2024)</u>](https://www.youtube.com/watch?v=yjxv1HuRQy0)
- [<u>Install Node.js in Windows 10/11</u>](https://www.youtube.com/watch?v=uCgAuOYpJd0)
> ⚠️ **Note:** If you run into permission issues during installation, try running your terminal or command prompt as an administrator.
🎉 You're all set! Dive in and start coding smarter and faster with **Cline**.
@@ -69,9 +69,9 @@ Choose your AI assistant based on your needs:
### Getting Started
1. Install the development essentials:
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/getting-started-new-coders/installing-dev-essentials)
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/installing-dev-essentials)
2. Set up Cline's Memory Bank:
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/improving-your-prompting-skills/custom-instructions-library/cline-memory-bank)
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/prompting/cline-memory-bank)
- Create an empty `cline_docs` folder in your project root
- Create `projectBrief.md` in the `cline_docs` folder (see example below)
- Tell Cline to "initialize memory bank"
@@ -197,20 +197,20 @@ git push origin main # Upload to GitHub
1. **Start of day**: Get latest changes
```bash
bashCopygit pull origin main # Download latest code
git pull origin main # Download latest code
```
2. **During development**: Save work regularly
```bash
bashCopygit add .
git add .
git commit -m "Clear message about changes"
```
3. **End of day**: Share your progress
```bash
bashCopygit push origin main # Upload to GitHub
git push origin main # Upload to GitHub
```
**Best Practices**
@@ -38,7 +38,7 @@ Cline actively builds context in two ways:
- Guide focus areas
- Share design thoughts and requirements
💡 **Key Point**: Cline isn't passive - it actively seeks to understand your project. You can either let it explore or guide its focus, especially in [Plan](https://docs.cline.bot/exploring-clines-tools/plan-and-act-modes-a-guide-to-effective-ai-development) mode.
💡 **Key Point**: Cline isn't passive - it actively seeks to understand your project. You can either let it explore or guide its focus, especially in [Plan](https://docs.cline.bot/features/plan-and-act) mode.
### Context & Context Windows
@@ -93,7 +93,7 @@ Context files help maintain understanding across sessions. They serve as documen
#### Approaches to Context Files
1. **Evergreen Project Context (i.e.** [**Memory Bank**](https://docs.cline.bot/improving-your-prompting-skills/custom-instructions-library/cline-memory-bank)**)**
1. **Evergreen Project Context (i.e.** [**Memory Bank**](https://docs.cline.bot/prompting/cline-memory-bank)**)**
- Living documentation that evolves with your project
- Updated as architecture and patterns emerge
- Example: The Memory Bank pattern maintains files like `techContext.md` and `systemPatterns.md`
@@ -151,7 +151,7 @@ Context files help maintain understanding across sessions. They serve as documen
- Use Plan mode for complex discussions
- Start fresh sessions when needed
3. **Team Projects**
- Share common context files (consider using [.clinerules](https://docs.cline.bot/improving-your-prompting-skills/prompting) files in project roots)
- Share common context files (consider using [.clinerules](https://docs.cline.bot/features/cline-rules) files in project roots)
- Document architectural decisions
- Maintain consistent patterns
- Keep documentation current
@@ -0,0 +1,174 @@
const { RuleTester: GrpcRuleTester } = require("eslint")
const grpcRule = require("../no-grpc-client-object-literals")
const grpcRuleTester = new GrpcRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
grpcRuleTester.run("no-grpc-client-object-literals", grpcRule, {
valid: [
// Valid case: Using .create() method with gRPC client
{
code: `
import { TogglePlanActModeRequest } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: PlanActMode.PLAN,
preferredLanguage: 'en',
},
})
);
`,
},
// Valid case: Using .fromPartial() method with gRPC client
{
code: `
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
const chatSettings = ChatSettings.fromPartial({
mode: PlanActMode.PLAN,
preferredLanguage: 'en',
});
StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: chatSettings,
})
);
`,
},
// Valid case: Regular function call with object literal (not a gRPC client)
{
code: `
function processData(data) {
console.log(data);
}
processData({
id: 123,
name: 'test',
});
`,
},
// Valid case: Using proper nested protobuf objects
{
code: `
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
// Using proper nested protobuf objects
const chatSettings = ChatSettings.create({
mode: 0,
preferredLanguage: 'en',
});
const request = TogglePlanActModeRequest.create({
chatSettings: chatSettings,
});
StateServiceClient.togglePlanActMode(request);
`,
},
// Valid case: Object literal in second parameter (should not be checked)
{
code: `
import { StateSubscribeRequest } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
const request = StateSubscribeRequest.create({
topics: ['apiConfig', 'tasks']
});
// Second parameter is an object literal but should not trigger the rule
StateServiceClient.subscribe(request, {
metadata: {
userId: 123,
sessionId: "abc-123"
}
});
`,
},
],
invalid: [
// Invalid case: Using object literal directly with gRPC client
{
code: `
import { StateServiceClient } from '../services/grpc-client';
StateServiceClient.togglePlanActMode({
chatSettings: {
mode: 0,
preferredLanguage: 'en',
},
});
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Invalid case: Using object literal with nested properties
{
code: `
import { ChatSettings } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
const chatSettings = ChatSettings.create({
mode: 0,
preferredLanguage: 'en',
});
StateServiceClient.togglePlanActMode({
chatSettings: {
mode: 1,
preferredLanguage: 'fr',
},
});
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Invalid case: Nested object literal in protobuf create method
{
code: `
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
import { StateServiceClient } from '../services/grpc-client';
// Using nested object literal instead of ChatSettings.create()
const request = TogglePlanActModeRequest.create({
chatSettings: {
mode: 0,
preferredLanguage: 'en',
},
});
StateServiceClient.togglePlanActMode(request);
`,
errors: [{ messageId: "useProtobufMethod" }],
},
// Invalid case: Object literal as first parameter to subscribe method
{
code: `
import { StateServiceClient } from '../services/grpc-client';
// First parameter is an object literal, which should trigger the rule
StateServiceClient.subscribe({
topics: ['apiConfig', 'tasks']
}, {
metadata: {
userId: 123,
sessionId: "abc-123"
}
});
`,
errors: [{ messageId: "useProtobufMethod" }],
},
],
})
+16
View File
@@ -0,0 +1,16 @@
// eslint-rules/index.js
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
module.exports = {
rules: {
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
},
configs: {
recommended: {
plugins: ["local"],
rules: {
"local/no-grpc-client-object-literals": "error",
},
},
},
}
@@ -0,0 +1,216 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
module.exports = createRule({
name: "no-grpc-client-object-literals",
meta: {
type: "problem",
docs: {
description:
"Enforce using .create() or .fromPartial() for gRPC service client parameters instead of object literals",
recommended: "error",
},
messages: {
useProtobufMethod:
"Use the appropriate protobuf .create() or .fromPartial() method instead of " +
"object literal for gRPC client parameters.\n" +
"Found: {{code}}\n" +
"gRPC client methods should always receive properly created protobuf objects.",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Check if a name matches the gRPC service client pattern using regex
// Must start with an uppercase letter and end with ServiceClient
const isGrpcServiceClient = (name) => {
return typeof name === "string" && /^[A-Z].*ServiceClient$/.test(name)
}
const safeObjectExpressions = new Map() // 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.set(node.arguments[0], { isProblematic: false })
}
},
// Track create/fromPartial calls that contain nested object literals
"CallExpression[callee.type='MemberExpression'][callee.property.name=/^(create|fromPartial)$/]"(node) {
if (node.arguments.length > 0 && node.arguments[0].type === "ObjectExpression") {
// Track problematic nested object literals
const nestedObjectLiterals = new Map() // Map of object expressions to their containing property paths
// Search for nested object literals
const queue = [
...node.arguments[0].properties.map((prop) => ({
property: prop,
path: prop.key && prop.key.name ? prop.key.name : "unknown",
})),
]
while (queue.length > 0) {
const { property, path } = queue.shift()
// Skip spread elements
if (property.type !== "Property") continue
// If this is an object literal, mark it as problematic
if (property.value.type === "ObjectExpression") {
nestedObjectLiterals.set(property.value, path)
// Add nested properties to queue
queue.push(
...property.value.properties.map((prop) => ({
property: prop,
path: `${path}.${prop.key && prop.key.name ? prop.key.name : "unknown"}`,
})),
)
}
}
// For each problematic nested object, track it with its path
nestedObjectLiterals.forEach((path, objectExpr) => {
safeObjectExpressions.set(objectExpr, {
isProblematic: true,
path: path,
parentNode: node,
})
})
}
},
// Check calls to gRPC service clients
"CallExpression[callee.type='MemberExpression']"(node) {
// Get the object (left side) of the member expression
const callee = node.callee
if (callee.object && callee.object.type === "Identifier") {
const objectName = callee.object.name
// Check if this is a call to one of our gRPC service clients
if (isGrpcServiceClient(objectName)) {
// Only check the first argument of gRPC service client calls
if (node.arguments.length > 0) {
const arg = node.arguments[0] // Only check the first parameter
if (arg.type === "ObjectExpression" && !safeObjectExpressions.has(arg)) {
// This is an object literal being passed directly to a gRPC client
const sourceCode = context.getSourceCode()
const callText = sourceCode.getText(node).trim()
context.report({
node: arg,
messageId: "useProtobufMethod",
data: {
code: callText,
},
})
} else if (arg.type === "ObjectExpression") {
// Search for nested object literals that aren't protected
const queue = [...arg.properties]
while (queue.length > 0) {
const property = queue.shift()
// Skip spread elements
if (property.type !== "Property") continue
// Check value
if (
property.value.type === "ObjectExpression" &&
!safeObjectExpressions.has(property.value)
) {
// Found a nested object literal
const sourceCode = context.getSourceCode()
const propertyText = sourceCode.getText(property).trim()
context.report({
node: property.value,
messageId: "useProtobufMethod",
data: {
code: `${objectName}.${callee.property.name}(... ${propertyText} ...)`,
},
})
}
// Add any nested properties to the queue
if (property.value.type === "ObjectExpression") {
queue.push(...property.value.properties)
}
}
} else if (arg.type === "Identifier") {
// This is a variable - check if it references a problematic protobuf object
const varName = arg.name
const sourceCode = context.getSourceCode()
const scope = sourceCode.getScope(node)
// Find the variable declaration
const variable = scope.variables.find((v) => v.name === varName)
if (variable && variable.references && variable.references.length > 0) {
// Look for definitions
const def = variable.defs.find(
(d) => d.node && d.node.type === "VariableDeclarator" && d.node.init,
)
if (
def &&
def.node.init.type === "CallExpression" &&
def.node.init.callee.type === "MemberExpression" &&
(def.node.init.callee.property.name === "create" ||
def.node.init.callee.property.name === "fromPartial")
) {
// Flag if we find problematic nested object literals in this create/fromPartial call
const callText = sourceCode.getText(node).trim()
const initCallText = sourceCode.getText(def.node.init).trim()
// Check for nested object literals in init node
let foundNestedLiteral = false
if (
def.node.init.arguments.length > 0 &&
def.node.init.arguments[0].type === "ObjectExpression"
) {
// Find any nested object literals
const queue = [...def.node.init.arguments[0].properties]
while (queue.length > 0 && !foundNestedLiteral) {
const property = queue.shift()
// Skip spread elements
if (property.type !== "Property") continue
if (property.value.type === "ObjectExpression") {
foundNestedLiteral = true
context.report({
node,
messageId: "useProtobufMethod",
data: {
code: `${callText} - using request created with nested object literal at: ${property.key.name}`,
},
})
}
// Add any nested properties to the queue
if (property.value.type === "ObjectExpression") {
queue.push(...property.value.properties)
}
}
}
}
}
}
}
}
}
},
}
},
})
+2479
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "eslint-plugin-eslint-rules",
"version": "1.0.0",
"description": "Custom ESLint rules for Cline",
"main": "index.js",
"scripts": {
"test": "mocha --no-config --require ts-node/register __tests__/**/*.test.ts"
},
"keywords": [
"eslint",
"eslintplugin"
],
"author": "Cline Bot Inc.",
"license": "Apache-2.0",
"dependencies": {
"@typescript-eslint/utils": "^8.33.0"
},
"devDependencies": {
"@types/eslint": "^8.0.0",
"@types/mocha": "^10.0.7",
"@types/node": "^20.0.0",
"@typescript-eslint/parser": "^7.14.1",
"eslint": "^8.57.0",
"mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"peerDependencies": {
"eslint": ">=8.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"resolveJsonModule": true,
"declaration": true
},
"include": ["**/*.ts", "**/*.js", "**/*.tsx", "__tests__/**/*"],
"exclude": ["node_modules", "dist"]
}
+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}`)
}
}
+3485 -1618
View File
File diff suppressed because it is too large Load Diff
+11 -12
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.4",
"version": "3.17.8",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -58,13 +58,7 @@
"id": "claude-dev-ActivityBar",
"title": "Cline (Ctrl+')",
"icon": "assets/icons/icon.svg",
"when": "isWindows"
},
{
"id": "claude-dev-ActivityBar",
"title": "Cline (Ctrl+')",
"icon": "assets/icons/icon.svg",
"when": "isLinux || !isMac && !isWindows"
"when": "!isMac"
}
]
},
@@ -283,9 +277,9 @@
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
"check-types": "npm run protos && tsc --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "npm-run-all test:unit test:integration",
@@ -322,13 +316,15 @@
"@types/turndown": "^5.0.5",
"@types/vscode": "^1.84.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/utils": "^8.33.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"eslint-plugin-eslint-rules": "file:eslint-rules",
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"mintlify": "^4.0.515",
@@ -347,8 +343,9 @@
"@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",
"@google/genai": "^0.13.0",
"@grpc/grpc-js": "^1.9.15",
@@ -362,6 +359,7 @@
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
@@ -371,6 +369,7 @@
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"diff": "^5.2.0",
"exceljs": "^4.4.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.2.0",
+3
View File
@@ -16,4 +16,7 @@ service AccountService {
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth callback events (when authentication tokens are received)
rpc subscribeToAuthCallback(EmptyRequest) returns (stream String);
}
+286 -4
View File
@@ -6,11 +6,44 @@ import { fileURLToPath } from "url"
import { execSync } from "child_process"
import { globby } from "globby"
import chalk from "chalk"
import os from "os"
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, "..")
@@ -39,14 +72,28 @@ 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..."))
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const HOST_TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto", "host")
// Create output directory if it doesn't exist
// Create output directories if they don't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
await fs.mkdir(HOST_TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
@@ -58,7 +105,7 @@ async function main() {
// Check for missing proto files for services in serviceNameMap
await ensureProtoFilesExist()
// Process all proto files
// Process main proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, realpath: true })
@@ -79,16 +126,42 @@ async function main() {
process.exit(1)
}
// Process host proto files
console.log(chalk.cyan("Processing host proto files from"), path.join(SCRIPT_DIR, "host"))
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host"), absolute: true })
if (hostProtoFiles.length > 0) {
// Build the protoc command for host proto files
const hostTsProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${path.join(SCRIPT_DIR, "host")}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
...hostProtoFiles,
].join(" ")
try {
console.log(chalk.cyan(`Generating TypeScript code for host proto files:\n${hostProtoFiles.join("\n")}...`))
execSync(hostTsProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating TypeScript for host proto files:"), error)
process.exit(1)
}
}
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
await fs.mkdir(descriptorOutDir, { recursive: true })
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
const allProtoFiles = [...protoFiles, ...hostProtoFiles]
const descriptorProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${path.join(SCRIPT_DIR, "host")}"`,
`--descriptor_set_out="${descriptorFile}"`,
"--include_imports",
...protoFiles,
...allProtoFiles,
].join(" ")
try {
console.log(chalk.cyan("Generating descriptor set..."))
@@ -102,8 +175,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()
}
/**
@@ -246,7 +322,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`
@@ -420,6 +496,212 @@ 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}`))
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
+5
View File
@@ -58,3 +58,8 @@ message Boolean {
message StringArray {
repeated string values = 1;
}
message StringArrays {
repeated string values1 = 1;
repeated string values2 = 2;
}
+6
View File
@@ -31,6 +31,9 @@ service FileService {
// Select images from the file system and return as data URLs
rpc selectImages(EmptyRequest) returns (StringArray);
// Select images and other files from the file system and returns as data URLs & paths respectively
rpc selectFiles(BooleanRequest) returns (StringArrays);
// Convert URIs to workspace-relative paths
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
@@ -49,6 +52,9 @@ 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);
}
// 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
}
+1
View File
@@ -15,6 +15,7 @@ service McpService {
rpc deleteMcpServer(StringRequest) returns (McpServers);
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
}
message ToggleMcpServerRequest {
+3
View File
@@ -1,5 +1,7 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
@@ -37,6 +39,7 @@ message ChatSettings {
message ChatContent {
optional string message = 1;
repeated string images = 2;
repeated string files = 3;
}
// Message for auto approval settings
+4
View File
@@ -11,6 +11,8 @@ service TaskService {
rpc cancelTask(EmptyRequest) returns (Empty);
// Clears the current task
rpc clearTask(EmptyRequest) returns (Empty);
// Gets the total size of all tasks
rpc getTotalTasksSize(EmptyRequest) returns (Int64);
// Deletes multiple tasks with the given IDs
rpc deleteTasksWithIds(StringArrayRequest) returns (Empty);
// Creates a new task with the given text and optional images
@@ -38,6 +40,7 @@ message NewTaskRequest {
Metadata metadata = 1;
string text = 2;
repeated string images = 3;
repeated string files = 4;
}
// Request message for toggling task favorite status
@@ -102,4 +105,5 @@ message AskResponseRequest {
string response_type = 2;
string text = 3;
repeated string images = 4;
repeated string files = 5;
}
+18
View File
@@ -6,6 +6,18 @@ 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;
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
@@ -13,4 +25,10 @@ service UiService {
// 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);
}
+1
View File
@@ -9,6 +9,7 @@ import "common.proto";
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
rpc openInBrowser(StringRequest) returns (Empty);
}
message IsImageUrl {
+3
View File
@@ -24,6 +24,7 @@ import { FireworksHandler } from "./providers/fireworks"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -84,6 +85,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new XAIHandler(options)
case "sambanova":
return new SambanovaHandler(options)
case "cerebras":
return new CerebrasHandler(options)
default:
return new AnthropicHandler(options)
}
+1 -1
View File
@@ -133,7 +133,7 @@ export class AnthropicHandler implements ApiHandler {
}
for await (const chunk of stream) {
switch (chunk.type) {
switch (chunk?.type) {
case "message_start":
// tells us cache reads/writes/input/output
const usage = chunk.message.usage
+13 -2
View File
@@ -120,7 +120,7 @@ export class AwsBedrockHandler implements ApiHandler {
)
for await (const chunk of stream) {
switch (chunk.type) {
switch (chunk?.type) {
case "message_start":
const usage = chunk.message.usage
yield {
@@ -223,8 +223,19 @@ export class AwsBedrockHandler implements ApiHandler {
secretAccessKey: string
sessionToken?: string
}> {
// Configure provider options
const providerOptions: any = {}
if (this.options.awsUseProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
providerOptions.ignoreCache = true
if (this.options.awsProfile) {
providerOptions.profile = this.options.awsProfile
}
}
// Create AWS credentials by executing an AWS provider chain
const providerChain = fromNodeProviderChain()
const providerChain = fromNodeProviderChain(providerOptions)
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
+169
View File
@@ -0,0 +1,169 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "@api/transform/stream"
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Cerebras
constructor(options: ApiHandlerOptions) {
this.options = options
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
content: string
}> = [{ role: "system", content: systemPrompt }]
// Convert Anthropic messages to Cerebras format
for (const message of messages) {
if (message.role === "user") {
const content = Array.isArray(message.content)
? message.content
.map((block) => {
if (block.type === "text") {
return block.text
} else if (block.type === "image") {
return "[Image content not supported in Cerebras]"
}
return ""
})
.join("\n")
: message.content
cerebrasMessages.push({ role: "user", content })
} else if (message.role === "assistant") {
const content = Array.isArray(message.content)
? message.content
.map((block) => {
if (block.type === "text") {
return block.text
}
return ""
})
.join("\n")
: message.content || ""
cerebrasMessages.push({ role: "assistant", content })
}
}
try {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
stream: true,
})
// Handle streaming response
let reasoning: string | null = null // Track reasoning content for models that support thinking
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
for await (const chunk of stream as any) {
// Type assertion for the streaming chunk
const streamChunk = chunk as any
if (streamChunk.choices?.[0]?.delta?.content) {
const content = streamChunk.choices[0].delta.content
// Handle reasoning models (Qwen and DeepSeek R1 Distill) that use <think> tags
if (isReasoningModel) {
// Check if we're entering or continuing reasoning mode
if (reasoning || content.includes("<think>")) {
reasoning = (reasoning || "") + content
// Clean the content by removing think tags for display
let cleanContent = content.replace(/<think>/g, "").replace(/<\/think>/g, "")
// Only yield reasoning content if there's actual content after cleaning
if (cleanContent.trim()) {
yield {
type: "reasoning",
reasoning: cleanContent,
}
}
// Check if reasoning is complete
if (reasoning.includes("</think>")) {
reasoning = null
}
} else {
// Regular content outside of thinking tags
yield {
type: "text",
text: content,
}
}
} else {
// Non-reasoning models - just yield text content
yield {
type: "text",
text: content,
}
}
}
// Handle usage information from Cerebras API
// Usage is typically only available in the final chunk
if (streamChunk.usage) {
const totalCost = this.calculateCost({
inputTokens: streamChunk.usage.prompt_tokens || 0,
outputTokens: streamChunk.usage.completion_tokens || 0,
})
yield {
type: "usage",
inputTokens: streamChunk.usage.prompt_tokens || 0,
outputTokens: streamChunk.usage.completion_tokens || 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost,
}
}
}
} catch (error) {
throw error
}
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in cerebrasModels) {
const id = modelId as CerebrasModelId
return { id, info: cerebrasModels[id] }
}
return {
id: cerebrasDefaultModelId,
info: cerebrasModels[cerebrasDefaultModelId],
}
}
private calculateCost({ inputTokens, outputTokens }: { inputTokens: number; outputTokens: number }): number {
const model = this.getModel()
const inputPrice = model.info.inputPrice || 0
const outputPrice = model.info.outputPrice || 0
const inputCost = (inputPrice / 1_000_000) * inputTokens
const outputCost = (outputPrice / 1_000_000) * outputTokens
return inputCost + outputCost
}
}
+3 -1
View File
@@ -80,7 +80,9 @@ export class OllamaHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.ollamaModelId || "",
info: openAiModelInfoSaneDefaults,
info: this.options.ollamaApiOptionsCtxNum
? { ...openAiModelInfoSaneDefaults, contextWindow: Number(this.options.ollamaApiOptionsCtxNum) || 32768 }
: openAiModelInfoSaneDefaults,
}
}
}
+4
View File
@@ -98,6 +98,10 @@ export class OpenAiHandler implements ApiHandler {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
+1 -1
View File
@@ -154,7 +154,7 @@ export class VertexHandler implements ApiHandler {
}
for await (const chunk of stream) {
switch (chunk.type) {
switch (chunk?.type) {
case "message_start":
const usage = chunk.message.usage
yield {
+2 -2
View File
@@ -58,10 +58,10 @@ export class XAIHandler implements ApiHandler {
if (chunk.usage) {
yield {
type: "usage",
inputTokens: 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
+145
View File
@@ -0,0 +1,145 @@
import { JSONParser } from "@streamparser/json"
import * as fs from "fs"
import * as path from "path"
import * as os from "os"
// Fallback type definition based on the error message: "Property 'value' is optional in type 'ParsedElementInfo'"
type ParsedElementInfo = {
value?: any
key?: string | number
parent?: any
stack?: any[]
}
export interface ReplacementItem {
old_string: string
new_string: string
}
export interface ChangeLocation {
startLine: number
endLine: number
startChar: number
endChar: number
}
export class StreamingJsonReplacer {
private currentFileContent: string
private parser: JSONParser
private onContentUpdated: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void
private onErrorCallback: (error: Error) => void
private itemsProcessed: number = 0
private successfullyParsedItems: ReplacementItem[] = []
constructor(
initialContent: string,
onContentUpdatedCallback: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void,
onErrorCallback: (error: Error) => void,
) {
// Initialize log file path
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
this.currentFileContent = initialContent
this.onContentUpdated = onContentUpdatedCallback
this.onErrorCallback = onErrorCallback
this.parser = new JSONParser({ paths: ["$.*"] })
this.parser.onValue = (parsedElementInfo: ParsedElementInfo) => {
const { value } = parsedElementInfo // Destructure to get value, which might be undefined
// This callback is triggered for each item matched by '$.replacements.*'
if (value && typeof value === "object" && "old_string" in value && "new_string" in value) {
const item = value as ReplacementItem // Value here is confirmed to be an object
if (typeof item.old_string === "string" && typeof item.new_string === "string") {
this.successfullyParsedItems.push(item) // Store the structurally valid item
if (this.currentFileContent.includes(item.old_string)) {
// Calculate the change location before making the replacement
const changeLocation = this.calculateChangeLocation(item.old_string, item.new_string)
const beforeLength = this.currentFileContent.length
this.currentFileContent = this.currentFileContent.replace(item.old_string, item.new_string)
const afterLength = this.currentFileContent.length
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.onContentUpdated(this.currentFileContent, false, changeLocation)
} else {
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.onErrorCallback(error) // Call our own error callback
}
} else {
const error = new Error(`Invalid item structure in replacements stream: ${JSON.stringify(item)}`)
this.onErrorCallback(error) // Call our own error callback
}
}
}
this.parser.onError = (err: Error) => {
// Propagate the error to the caller via the callback
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.
}
}
public write(jsonChunk: string): void {
try {
// Errors during write will be caught by the parser's onError or thrown.
this.parser.write(jsonChunk)
} catch (error) {
throw error
}
}
public getCurrentContent(): string {
return this.currentFileContent
}
public getSuccessfullyParsedItems(): ReplacementItem[] {
return [...this.successfullyParsedItems] // Return a copy
}
private calculateChangeLocation(oldStr: string, newStr: string): ChangeLocation {
// Find the index where the old string starts
const startIndex = this.currentFileContent.indexOf(oldStr)
if (startIndex === -1) {
// 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)
const startLine = (contentBeforeStart.match(/\n/g) || []).length
// Calculate the end index after replacement
const endIndex = startIndex + oldStr.length
const contentBeforeEnd = this.currentFileContent.substring(0, endIndex)
const endLine = (contentBeforeEnd.match(/\n/g) || []).length
// Calculate character positions within their respective lines
const lastNewlineBeforeStart = contentBeforeStart.lastIndexOf("\n")
const startChar = lastNewlineBeforeStart === -1 ? startIndex : startIndex - lastNewlineBeforeStart - 1
const lastNewlineBeforeEnd = contentBeforeEnd.lastIndexOf("\n")
const endChar = lastNewlineBeforeEnd === -1 ? endIndex : endIndex - lastNewlineBeforeEnd - 1
const result = {
startLine,
endLine,
startChar,
endChar,
}
return result
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
export type AssistantMessageContent = TextContent | ToolUse
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
export { parseAssistantMessageV1, parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
export interface TextContent {
type: "text"
@@ -27,6 +27,7 @@ export const toolUseNames = [
"condense",
"report_bug",
"new_rule",
"web_fetch",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -473,3 +473,621 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
return contentBlocks
}
export function parseAssistantMessageV3(assistantMessage: string): AssistantMessageContent[] {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContentStart = 0 // Index where the current text block started
let currentTextContent: TextContent | undefined = undefined
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
let currentToolUse: ToolUse | undefined = undefined
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
let currentParamName: ToolParamName | undefined = undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
}
for (const name of toolParamNames) {
toolParamOpenTags.set(`<${name}>`, name)
}
// Function calls format detection
const isFunctionCallsOpen = "<function_calls>"
const isFunctionCallsClose = "</function_calls>"
const isInvokeStart = '<invoke name="'
const isInvokeEnd = '">'
const isInvokeClose = "</invoke>"
const isParameterStart = '<parameter name="'
const isParameterNameEnd = '">'
const isParameterClose = "</parameter>"
// Variables for function calls parsing
let inFunctionCalls = false
let currentInvokeName = ""
let currentParameterName = ""
const len = assistantMessage.length
for (let i = 0; i < len; i++) {
const currentCharIndex = i
// --- State: Parsing Function Calls ---
// Check for opening function_calls tag
if (
!inFunctionCalls &&
currentCharIndex >= isFunctionCallsOpen.length - 1 &&
assistantMessage.startsWith(isFunctionCallsOpen, currentCharIndex - isFunctionCallsOpen.length + 1)
) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart, currentCharIndex - isFunctionCallsOpen.length + 1)
.trim()
currentTextContent.partial = false
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
}
inFunctionCalls = true
continue
}
// Check for invoke start within function_calls
if (
inFunctionCalls &&
currentInvokeName === "" &&
!currentToolUse && // Don't create a new tool if we already have one
currentCharIndex >= isInvokeStart.length - 1 &&
assistantMessage.startsWith(isInvokeStart, currentCharIndex - isInvokeStart.length + 1)
) {
// Find the end of the invoke name
const nameEndPos = assistantMessage.indexOf(isInvokeEnd, currentCharIndex + 1)
if (nameEndPos !== -1) {
// Extract the invoke name
currentInvokeName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
i = nameEndPos + isInvokeEnd.length - 1 // Skip to after the '">
// If this is an LS invoke, create a list_files tool
if (currentInvokeName === "LS") {
currentToolUse = {
type: "tool_use",
name: "list_files",
params: {},
partial: true,
}
}
// If this is a Grep invoke, create a search_files tool
if (currentInvokeName === "Grep") {
currentToolUse = {
type: "tool_use",
name: "search_files",
params: {},
partial: true,
}
}
if (currentInvokeName === "Bash") {
currentToolUse = {
type: "tool_use",
name: "execute_command",
params: {},
partial: true,
}
}
if (currentInvokeName === "Read") {
currentToolUse = {
type: "tool_use",
name: "read_file",
params: {},
partial: true,
}
}
if (currentInvokeName === "Write") {
currentToolUse = {
type: "tool_use",
name: "write_to_file",
params: {},
partial: true,
}
}
if (currentInvokeName === "WebFetch") {
currentToolUse = {
type: "tool_use",
name: "web_fetch",
params: {},
partial: true,
}
}
if (currentInvokeName === "AskQuestion") {
currentToolUse = {
type: "tool_use",
name: "ask_followup_question",
params: {},
partial: true,
}
}
if (currentInvokeName === "UseMCPTool") {
currentToolUse = {
type: "tool_use",
name: "use_mcp_tool",
params: {},
partial: true,
}
}
if (currentInvokeName === "AccessMCPResource") {
currentToolUse = {
type: "tool_use",
name: "access_mcp_resource",
params: {},
partial: true,
}
}
if (currentInvokeName === "ListCodeDefinitionNames") {
currentToolUse = {
type: "tool_use",
name: "list_code_definition_names",
params: {},
partial: true,
}
}
if (currentInvokeName === "PlanModeRespond") {
currentToolUse = {
type: "tool_use",
name: "plan_mode_respond",
params: {},
partial: true,
}
}
if (currentInvokeName === "LoadMcpDocumentation") {
currentToolUse = {
type: "tool_use",
name: "load_mcp_documentation",
params: {},
partial: true,
}
}
if (currentInvokeName === "AttemptCompletion") {
currentToolUse = {
type: "tool_use",
name: "attempt_completion",
params: {},
partial: true,
}
}
if (currentInvokeName === "BrowserAction") {
currentToolUse = {
type: "tool_use",
name: "browser_action",
params: {},
partial: true,
}
}
if (currentInvokeName === "NewTask") {
currentToolUse = {
type: "tool_use",
name: "new_task",
params: {},
partial: true,
}
}
// If this is a MultiEdit invoke, create a replace_in_file tool
if (currentInvokeName === "MultiEdit") {
currentToolUse = {
type: "tool_use",
name: "replace_in_file",
params: {},
partial: true,
}
}
continue
}
}
// Check for parameter start within invoke
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentParameterName === "" &&
currentCharIndex >= isParameterStart.length - 1 &&
assistantMessage.startsWith(isParameterStart, currentCharIndex - isParameterStart.length + 1)
) {
// Find the end of the parameter name
const nameEndPos = assistantMessage.indexOf(isParameterNameEnd, currentCharIndex + 1)
if (nameEndPos !== -1) {
// Extract the parameter name
currentParameterName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
currentParamValueStart = nameEndPos + isParameterNameEnd.length
i = nameEndPos + isParameterNameEnd.length - 1 // Skip to after the '">'
continue
}
}
// Check for parameter end
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentParameterName !== "" &&
currentCharIndex >= isParameterClose.length - 1 &&
assistantMessage.startsWith(isParameterClose, currentCharIndex - isParameterClose.length + 1)
) {
// Extract parameter value
const value = assistantMessage.slice(currentParamValueStart, currentCharIndex - isParameterClose.length + 1).trim()
// Map parameter to tool params
if (currentToolUse && currentInvokeName === "LS" && currentParameterName === "path") {
currentToolUse.params["path"] = value
// Default recursive to false - only show top level
currentToolUse.params["recursive"] = "false"
}
if (currentToolUse && currentInvokeName === "Read" && currentParameterName === "file_path") {
currentToolUse.params["path"] = value
}
if (currentToolUse && currentInvokeName === "PlanModeRespond" && currentParameterName === "response") {
currentToolUse.params["response"] = value
}
if (currentToolUse && currentInvokeName === "WebFetch" && currentParameterName === "url") {
currentToolUse.params["url"] = value
}
if (currentToolUse && currentInvokeName === "ListCodeDefinitionNames" && currentParameterName === "path") {
currentToolUse.params["path"] = value
}
if (currentToolUse && currentInvokeName === "NewTask" && currentParameterName === "context") {
currentToolUse.params["context"] = value
}
// Map parameter to tool params for Grep
if (currentToolUse && currentInvokeName === "Grep") {
if (currentParameterName === "pattern") {
currentToolUse.params["regex"] = value
} else if (currentParameterName === "path") {
currentToolUse.params["path"] = value
} else if (currentParameterName === "include") {
currentToolUse.params["file_pattern"] = value
}
}
if (currentToolUse && currentInvokeName === "Bash") {
if (currentParameterName === "command") {
currentToolUse.params["command"] = value
} else if (currentParameterName === "requires_approval") {
currentToolUse.params["requires_approval"] = value === "true" ? "true" : "false"
}
}
if (currentToolUse && currentInvokeName === "Write") {
if (currentParameterName === "file_path") {
currentToolUse.params["path"] = value
} else if (currentParameterName === "content") {
currentToolUse.params["content"] = value
}
}
if (currentToolUse && currentInvokeName === "AskQuestion") {
if (currentParameterName === "question") {
currentToolUse.params["question"] = value
} else if (currentParameterName === "options") {
currentToolUse.params["options"] = value
}
}
if (currentToolUse && currentInvokeName === "UseMCPTool") {
if (currentParameterName === "server_name") {
currentToolUse.params["server_name"] = value
} else if (currentParameterName === "tool_name") {
currentToolUse.params["tool_name"] = value
} else if (currentParameterName === "arguments") {
currentToolUse.params["arguments"] = value
}
}
if (currentToolUse && currentInvokeName === "AccessMCPResource") {
if (currentParameterName === "server_name") {
currentToolUse.params["server_name"] = value
} else if (currentParameterName === "uri") {
currentToolUse.params["uri"] = value
}
}
if (currentToolUse && currentInvokeName === "AttemptCompletion") {
if (currentParameterName === "result") {
currentToolUse.params["result"] = value
}
if (currentParameterName === "command") {
currentToolUse.params["command"] = value
}
}
if (currentToolUse && currentInvokeName === "BrowserAction") {
if (currentParameterName === "action") {
currentToolUse.params["action"] = value
} else if (currentParameterName === "url") {
currentToolUse.params["url"] = value
} else if (currentParameterName === "coordinate") {
currentToolUse.params["coordinate"] = value
} else if (currentParameterName === "text") {
currentToolUse.params["text"] = value
}
}
// Map parameter to tool params for MultiEdit
if (currentToolUse && currentInvokeName === "MultiEdit") {
if (currentParameterName === "file_path") {
currentToolUse.params["path"] = value
} else if (currentParameterName === "edits") {
// Save the value to the diff parameter for replace_in_file
currentToolUse.params["diff"] = value
}
}
currentParameterName = ""
continue
}
// Check for invoke end
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentCharIndex >= isInvokeClose.length - 1 &&
assistantMessage.startsWith(isInvokeClose, currentCharIndex - isInvokeClose.length + 1)
) {
// If we have a tool use from this invoke, finalize it
if (
currentToolUse &&
(currentInvokeName === "LS" ||
currentInvokeName === "Grep" ||
currentInvokeName === "Bash" ||
currentInvokeName === "Read" ||
currentInvokeName === "Write" ||
currentInvokeName === "WebFetch" ||
currentInvokeName === "AskQuestion" ||
currentInvokeName === "UseMCPTool" ||
currentInvokeName === "AccessMCPResource" ||
currentInvokeName === "ListCodeDefinitionNames" ||
currentInvokeName === "PlanModeRespond" ||
currentInvokeName === "LoadMcpDocumentation" ||
currentInvokeName === "AttemptCompletion" ||
currentInvokeName === "BrowserAction" ||
currentInvokeName === "NewTask" ||
currentInvokeName === "MultiEdit")
) {
currentToolUse.partial = false
contentBlocks.push(currentToolUse)
currentToolUse = undefined
}
currentInvokeName = ""
continue
}
// Check for function_calls end
if (
inFunctionCalls &&
currentCharIndex >= isFunctionCallsClose.length - 1 &&
assistantMessage.startsWith(isFunctionCallsClose, currentCharIndex - isFunctionCallsClose.length + 1)
) {
inFunctionCalls = false
currentTextContentStart = currentCharIndex + 1
// Start a new text content block for any text after function_calls
currentTextContent = {
type: "text",
content: "",
partial: true,
}
continue
}
// Skip normal parsing when inside function_calls
if (inFunctionCalls) {
continue
}
// --- State: Parsing a Tool Parameter ---
if (currentToolUse && currentParamName) {
const closeTag = `</${currentParamName}>`
// Check if the string *ending* at index `i` matches the closing tag
if (
currentCharIndex >= closeTag.length - 1 &&
assistantMessage.startsWith(
closeTag,
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
)
) {
// Found the closing tag for the parameter
const value = assistantMessage
.slice(
currentParamValueStart, // Start after the opening tag
currentCharIndex - closeTag.length + 1, // End before the closing tag
)
.trim()
currentToolUse.params[currentParamName] = value
currentParamName = undefined // Go back to parsing tool content
// We don't continue loop here, need to check for tool close or other params at index i
} else {
continue // Still inside param value, move to next char
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
if (currentToolUse && !currentParamName) {
// Ensure we are not inside a parameter already
// Check if starting a new parameter
let startedNewParam = false
for (const [tag, paramName] of toolParamOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
currentParamName = paramName
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
startedNewParam = true
break
}
}
if (startedNewParam) {
continue // Handled start of param, move to next char
}
// Check if closing the current tool use
const toolCloseTag = `</${currentToolUse.name}>`
if (
currentCharIndex >= toolCloseTag.length - 1 &&
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
) {
// End of the tool use found
// Special handling for content params *before* finalizing the tool
const toolContentSlice = assistantMessage.slice(
currentToolUseStart, // From after the tool opening tag
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
)
// Check if content parameter needs special handling (write_to_file/new_rule)
// This check is important if the closing </content> tag was missed by the parameter parsing logic
// (e.g., if content is empty or parsing logic prioritizes tool close)
const contentParamName: ToolParamName = "content"
if (
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
toolContentSlice.includes(`<${contentParamName}>`)
) {
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStart = toolContentSlice.indexOf(contentStartTag)
// Use lastIndexOf for robustness against nested tags
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
currentToolUse.params[contentParamName] = contentValue
}
}
currentToolUse.partial = false // Mark as complete
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Reset state
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
continue // Move to next char
}
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
continue
}
// --- State: Parsing Text / Looking for Tool Start ---
if (!currentToolUse) {
// Check if starting a new tool use
let startedNewTool = false
for (const [tag, toolName] of toolUseOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(
currentTextContentStart, // From where text started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
currentTextContent.partial = false // Ended because tool started
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
} else {
// Check for any text between the last block and this tag
const potentialText = assistantMessage
.slice(
currentTextContentStart, // From where text *might* have started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false,
})
}
}
// Start the new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
params: {},
partial: true, // Assume partial until closing tag is found
}
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
startedNewTool = true
break
}
}
if (startedNewTool) {
continue // Handled start of tool, move to next char
}
// If not starting a tool, it must be text content
if (!currentTextContent) {
// Start a new text block if we aren't already in one
currentTextContentStart = currentCharIndex // Text starts at the current character
// Check if the current char is the start of potential text *immediately* after a tag
// This needs the previous state - simpler to let slicing handle it later.
// Resetting start index accurately is key.
// It should be the index *after* the last processed tag.
// The logic managing currentTextContentStart after closing tags handles this.
currentTextContent = {
type: "text",
content: "", // Will be determined by slicing at the end or when a tool starts
partial: true,
}
}
// Continue accumulating text implicitly; content is extracted later.
}
} // End of loop
// --- Finalization after loop ---
// Finalize any open parameter within an open tool use
if (currentToolUse && currentParamName) {
currentToolUse.params[currentParamName] = assistantMessage
.slice(currentParamValueStart) // From param start to end of string
.trim()
// Tool use remains partial
}
// Finalize any open tool use (which might contain the finalized partial param)
if (currentToolUse) {
// Tool use is partial because the loop finished before its closing tag
contentBlocks.push(currentToolUse)
}
// Finalize any trailing text content
// Only possible if a tool use wasn't open at the very end
else if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart) // From text start to end of string
.trim()
// Text is partial because the loop finished
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
}
return contentBlocks
}
@@ -12,7 +12,7 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
@@ -27,7 +27,7 @@ export async function accountLoginClicked(controller: Controller, unused: EmptyR
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return {
return String.create({
value: authUrl.toString(),
}
})
}
@@ -1,4 +1,4 @@
import type { Empty } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import type { EmptyRequest } from "../../../shared/proto/common"
import type { Controller } from "../index"
@@ -10,5 +10,5 @@ import type { Controller } from "../index"
*/
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
await controller.handleSignOut()
return {}
return Empty.create({})
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create account service registry
const accountService = createServiceRegistry("account")
// Export the method handler types and registration function
export type AccountMethodHandler = ServiceMethodHandler
export type AccountStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = accountService.registerMethod
// Export the request handlers
export const handleAccountServiceRequest = accountService.handleRequest
export const handleAccountServiceStreamingRequest = accountService.handleStreamingRequest
export const isStreamingMethod = accountService.isStreamingMethod
// Register all account methods
registerAllMethods()
-14
View File
@@ -1,14 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { accountLoginClicked } from "./accountLoginClicked"
import { accountLogoutClicked } from "./accountLogoutClicked"
// Register all account service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("accountLoginClicked", accountLoginClicked)
registerMethod("accountLogoutClicked", accountLogoutClicked)
}
@@ -0,0 +1,59 @@
import { Controller } from "../index"
import { EmptyRequest } from "../../../shared/proto/common"
import { String as ProtoString } from "../../../shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active authCallback subscriptions
const activeAuthCallbackSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to authCallback 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 subscribeToAuthCallback(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeAuthCallbackSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeAuthCallbackSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "authCallback_subscription" }, responseStream)
}
}
/**
* Send an authCallback event to all active subscribers
* @param customToken The custom token for authentication
*/
export async function sendAuthCallbackEvent(customToken: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeAuthCallbackSubscriptions).map(async (responseStream) => {
try {
const event: ProtoString = {
value: customToken,
}
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending authCallback event:", error)
// Remove the subscription if there was an error
activeAuthCallbackSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -24,24 +24,24 @@ export async function discoverBrowser(controller: Controller, request: EmptyRequ
const browserSession = new BrowserSession(controller.context, browserSettings)
const result = await browserSession.testConnection(discoveredHost)
return {
return BrowserConnection.create({
success: true,
message: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
endpoint: result.endpoint || "",
}
})
} else {
return {
return BrowserConnection.create({
success: false,
message:
"No Chrome instances found. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
}
})
}
} catch (error) {
return {
return BrowserConnection.create({
success: false,
message: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
})
}
}
@@ -9,7 +9,7 @@ import { getAllExtensionState } from "@core/storage/state"
* @param request The request message
* @returns The browser connection info
*/
export async function getBrowserConnectionInfo(controller: Controller, request: EmptyRequest): Promise<BrowserConnectionInfo> {
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const { browserSettings } = await getAllExtensionState(controller.context)
@@ -23,25 +23,25 @@ export async function getBrowserConnectionInfo(controller: Controller, request:
const connectionInfo = browserSession.getConnectionInfo()
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
return {
return BrowserConnectionInfo.create({
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host || "", // Ensure host is never undefined
}
})
}
// Fallback to browser settings if no active browser session
return {
return BrowserConnectionInfo.create({
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost || "",
}
})
} catch (error: unknown) {
console.error("Error getting browser connection info:", error)
return {
return BrowserConnectionInfo.create({
isConnected: false,
isRemote: false,
host: "",
}
})
}
}
@@ -10,21 +10,21 @@ import { BrowserSession } from "../../../services/browser/BrowserSession"
* @param request The empty request message
* @returns The detected Chrome path and whether it's bundled
*/
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
try {
const { browserSettings } = await getAllExtensionState(controller.context)
const browserSession = new BrowserSession(controller.context, browserSettings)
const result = await browserSession.getDetectedChromePath()
return {
return ChromePath.create({
path: result.path,
isBundled: result.isBundled,
}
})
} catch (error) {
console.error("Error getting detected Chrome path:", error)
return {
return ChromePath.create({
path: "",
isBundled: false,
}
})
}
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create browser service registry
const browserService = createServiceRegistry("browser")
// Export the method handler types and registration function
export type BrowserMethodHandler = ServiceMethodHandler
export type BrowserStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = browserService.registerMethod
// Export the request handlers
export const handleBrowserServiceRequest = browserService.handleRequest
export const handleBrowserServiceStreamingRequest = browserService.handleStreamingRequest
export const isStreamingMethod = browserService.isStreamingMethod
// Register all browser methods
registerAllMethods()
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { discoverBrowser } from "./discoverBrowser"
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
import { getDetectedChromePath } from "./getDetectedChromePath"
import { relaunchChromeDebugMode } from "./relaunchChromeDebugMode"
import { testBrowserConnection } from "./testBrowserConnection"
import { updateBrowserSettings } from "./updateBrowserSettings"
// Register all browser service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("discoverBrowser", discoverBrowser)
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
registerMethod("getDetectedChromePath", getDetectedChromePath)
registerMethod("relaunchChromeDebugMode", relaunchChromeDebugMode)
registerMethod("testBrowserConnection", testBrowserConnection)
registerMethod("updateBrowserSettings", updateBrowserSettings)
}
@@ -8,7 +8,7 @@ import { BrowserSession } from "../../../services/browser/BrowserSession"
* @param request The empty request message
* @returns The browser relaunch result as a string message
*/
export async function relaunchChromeDebugMode(controller: Controller, request: EmptyRequest): Promise<StringMessage> {
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
try {
const { browserSettings } = await controller.getStateToPostToWebview()
const browserSession = new BrowserSession(controller.context, browserSettings)
@@ -18,9 +18,9 @@ export async function relaunchChromeDebugMode(controller: Controller, request: E
// The actual result will be sent via postMessageToWebview in the BrowserSession.relaunchChromeDebugMode method
// Here we just return a message as a placeholder
return {
return StringMessage.create({
value: "Chrome relaunch initiated",
}
})
} catch (error) {
throw new Error(`Error relaunching Chrome: ${error instanceof Error ? error.message : globalThis.String(error)}`)
}
@@ -24,40 +24,40 @@ export async function testBrowserConnection(controller: Controller, request: Str
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
return {
return BrowserConnection.create({
success: result.success,
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint || "",
}
})
} else {
return {
return BrowserConnection.create({
success: false,
message:
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
}
})
}
} catch (error) {
return {
return BrowserConnection.create({
success: false,
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
})
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(text)
return {
return BrowserConnection.create({
success: result.success,
message: result.message,
endpoint: result.endpoint || "",
}
})
}
} catch (error) {
return {
return BrowserConnection.create({
success: false,
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
})
}
}
@@ -50,13 +50,13 @@ export async function updateBrowserSettings(controller: Controller, request: Upd
// Post updated state to webview
await controller.postStateToWebview()
return {
return Boolean.create({
value: true,
}
})
} catch (error) {
console.error("Error updating browser settings:", error)
return {
return Boolean.create({
value: false,
}
})
}
}
@@ -18,5 +18,5 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
await controller.task?.restoreCheckpoint(request.number, request.restoreType as ClineCheckpointRestore, request.offset)
}
return {}
return Empty.create({})
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create checkpoints service registry
const checkpointsService = createServiceRegistry("checkpoints")
// Export the method handler types and registration function
export type CheckpointsMethodHandler = ServiceMethodHandler
export type CheckpointsStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = checkpointsService.registerMethod
// Export the request handlers
export const handleCheckpointsServiceRequest = checkpointsService.handleRequest
export const handleCheckpointsServiceStreamingRequest = checkpointsService.handleStreamingRequest
export const isStreamingMethod = checkpointsService.isStreamingMethod
// Register all checkpoints methods
registerAllMethods()
@@ -1,14 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { checkpointDiff } from "./checkpointDiff"
import { checkpointRestore } from "./checkpointRestore"
// Register all checkpoints service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("checkpointDiff", checkpointDiff)
registerMethod("checkpointRestore", checkpointRestore)
}
+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
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create file service registry
const fileService = createServiceRegistry("file")
// Export the method handler types and registration function
export type FileMethodHandler = ServiceMethodHandler
export type FileStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = fileService.registerMethod
// Export the request handlers
export const handleFileServiceRequest = fileService.handleRequest
export const handleFileServiceStreamingRequest = fileService.handleStreamingRequest
export const isStreamingMethod = fileService.isStreamingMethod
// Register all file methods
registerAllMethods()
-38
View File
@@ -1,38 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { copyToClipboard } from "./copyToClipboard"
import { createRuleFile } from "./createRuleFile"
import { deleteRuleFile } from "./deleteRuleFile"
import { getRelativePaths } from "./getRelativePaths"
import { openFile } from "./openFile"
import { openImage } from "./openImage"
import { openMention } from "./openMention"
import { refreshRules } from "./refreshRules"
import { searchCommits } from "./searchCommits"
import { searchFiles } from "./searchFiles"
import { selectImages } from "./selectImages"
import { toggleClineRule } from "./toggleClineRule"
import { toggleCursorRule } from "./toggleCursorRule"
import { toggleWindsurfRule } from "./toggleWindsurfRule"
// Register all file service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("copyToClipboard", copyToClipboard)
registerMethod("createRuleFile", createRuleFile)
registerMethod("deleteRuleFile", deleteRuleFile)
registerMethod("getRelativePaths", getRelativePaths)
registerMethod("openFile", openFile)
registerMethod("openImage", openImage)
registerMethod("openMention", openMention)
registerMethod("refreshRules", refreshRules)
registerMethod("searchCommits", searchCommits)
registerMethod("searchFiles", searchFiles)
registerMethod("selectImages", selectImages)
registerMethod("toggleClineRule", toggleClineRule)
registerMethod("toggleCursorRule", toggleCursorRule)
registerMethod("toggleWindsurfRule", toggleWindsurfRule)
}
@@ -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()
}
+2 -2
View File
@@ -18,14 +18,14 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
return {
return RefreshedRules.create({
globalClineRulesToggles: { toggles: globalToggles },
localClineRulesToggles: { toggles: localToggles },
localCursorRulesToggles: { toggles: cursorLocalToggles },
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
localWorkflowToggles: { toggles: localWorkflowToggles },
globalWorkflowToggles: { toggles: globalWorkflowToggles },
}
})
} catch (error) {
console.error("Failed to refresh rules:", error)
throw error
+21
View File
@@ -0,0 +1,21 @@
import { Controller } from ".."
import { BooleanRequest, StringArrays } from "@shared/proto/common"
import { selectFiles as selectFilesIntegration } from "@integrations/misc/process-files"
import { FileMethodHandler } from "./index"
/**
* Prompts the user to select images from the file system and returns them as data URLs
* @param controller The controller instance
* @param request Boolean request, with the value defining whether this model supports images
* @returns Two arrays of image data URLs and other file paths
*/
export const selectFiles: FileMethodHandler = async (controller: Controller, request: BooleanRequest): Promise<StringArrays> => {
try {
const { images, files } = await selectFilesIntegration(request.value)
return StringArrays.create({ values1: images, values2: files })
} catch (error) {
console.error("Error selecting images & files:", error)
// Return empty array on error
return StringArrays.create({ values1: [], values2: [] })
}
}
+4 -3
View File
@@ -1,4 +1,5 @@
import type { ToggleClineRuleRequest, ClineRulesToggles, ToggleClineRules } from "../../../shared/proto/file"
import { ToggleClineRules } from "../../../shared/proto/file"
import type { ToggleClineRuleRequest } from "../../../shared/proto/file"
import type { Controller } from "../index"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
@@ -36,8 +37,8 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
const globalToggles = ((await getGlobalState(controller.context, "globalClineRulesToggles")) as AppClineRulesToggles) || {}
const localToggles = ((await getWorkspaceState(controller.context, "localClineRulesToggles")) as AppClineRulesToggles) || {}
return {
return ToggleClineRules.create({
globalClineRulesToggles: { toggles: globalToggles },
localClineRulesToggles: { toggles: localToggles },
}
})
}
+4 -3
View File
@@ -1,4 +1,5 @@
import type { ToggleCursorRuleRequest, ClineRulesToggles } from "../../../shared/proto/file"
import type { ToggleCursorRuleRequest } from "../../../shared/proto/file"
import { ClineRulesToggles } from "../../../shared/proto/file"
import type { Controller } from "../index"
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
@@ -28,7 +29,7 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
// Get the current state to return in the response
const cursorToggles = ((await getWorkspaceState(controller.context, "localCursorRulesToggles")) as AppClineRulesToggles) || {}
return {
return ClineRulesToggles.create({
toggles: cursorToggles,
}
})
}
@@ -1,4 +1,5 @@
import type { ToggleWindsurfRuleRequest, ClineRulesToggles } from "../../../shared/proto/file"
import type { ToggleWindsurfRuleRequest } from "../../../shared/proto/file"
import { ClineRulesToggles } from "../../../shared/proto/file"
import type { Controller } from "../index"
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
@@ -26,5 +27,5 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
await updateWorkspaceState(controller.context, "localWindsurfRulesToggles", toggles)
// Return the toggles directly
return { toggles: toggles }
return ClineRulesToggles.create({ toggles: toggles })
}
@@ -1,80 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
import { handleAccountServiceRequest, handleAccountServiceStreamingRequest } from "./account/index"
import { handleBrowserServiceRequest, handleBrowserServiceStreamingRequest } from "./browser/index"
import { handleCheckpointsServiceRequest, handleCheckpointsServiceStreamingRequest } from "./checkpoints/index"
import { handleFileServiceRequest, handleFileServiceStreamingRequest } from "./file/index"
import { handleMcpServiceRequest, handleMcpServiceStreamingRequest } from "./mcp/index"
import { handleStateServiceRequest, handleStateServiceStreamingRequest } from "./state/index"
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
import { handleUiServiceRequest, handleUiServiceStreamingRequest } from "./ui/index"
/**
* Configuration for a service handler
*/
export interface ServiceHandlerConfig {
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>
streamingHandler: (
controller: Controller,
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => Promise<void>
}
/**
* Map of service names to their handler configurations
*/
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
"cline.AccountService": {
requestHandler: handleAccountServiceRequest,
streamingHandler: handleAccountServiceStreamingRequest,
},
"cline.BrowserService": {
requestHandler: handleBrowserServiceRequest,
streamingHandler: handleBrowserServiceStreamingRequest,
},
"cline.CheckpointsService": {
requestHandler: handleCheckpointsServiceRequest,
streamingHandler: handleCheckpointsServiceStreamingRequest,
},
"cline.FileService": {
requestHandler: handleFileServiceRequest,
streamingHandler: handleFileServiceStreamingRequest,
},
"cline.McpService": {
requestHandler: handleMcpServiceRequest,
streamingHandler: handleMcpServiceStreamingRequest,
},
"cline.StateService": {
requestHandler: handleStateServiceRequest,
streamingHandler: handleStateServiceStreamingRequest,
},
"cline.TaskService": {
requestHandler: handleTaskServiceRequest,
streamingHandler: handleTaskServiceStreamingRequest,
},
"cline.WebService": {
requestHandler: handleWebServiceRequest,
streamingHandler: handleWebServiceStreamingRequest,
},
"cline.ModelsService": {
requestHandler: handleModelsServiceRequest,
streamingHandler: handleModelsServiceStreamingRequest,
},
"cline.SlashService": {
requestHandler: handleSlashServiceRequest,
streamingHandler: handleSlashServiceStreamingRequest,
},
"cline.UiService": {
requestHandler: handleUiServiceRequest,
streamingHandler: handleUiServiceStreamingRequest,
},
}
+29 -107
View File
@@ -23,7 +23,7 @@ import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Invoke, Platform } from "@shared/ExtensionMessage"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
@@ -52,6 +52,8 @@ import {
import { Task, cwd } 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"
@@ -107,9 +109,7 @@ export class Controller {
- https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
*/
async dispose() {
this.outputChannel.appendLine("Disposing ClineProvider...")
await this.clearTask()
this.outputChannel.appendLine("Cleared task")
while (this.disposables.length) {
const x = this.disposables.pop()
if (x) {
@@ -118,7 +118,6 @@ export class Controller {
}
this.workspaceTracker.dispose()
this.mcpHub.dispose()
this.outputChannel.appendLine("Disposed all disposables")
console.error("Controller disposed")
}
@@ -140,7 +139,7 @@ export class Controller {
await updateGlobalState(this.context, "userInfo", info)
}
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const {
apiConfiguration,
@@ -187,6 +186,7 @@ export class Controller {
customInstructions,
task,
images,
files,
historyItem,
)
}
@@ -194,7 +194,7 @@ export class Controller {
async reinitExistingTaskFromId(taskId: string) {
const history = await this.getTaskWithId(taskId)
if (history) {
await this.initTask(undefined, undefined, history.historyItem)
await this.initTask(undefined, undefined, undefined, history.historyItem)
}
}
@@ -261,21 +261,7 @@ export class Controller {
}
}
})
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "showChatView": {
this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
break
}
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
@@ -285,7 +271,7 @@ export class Controller {
// 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)
await this.initTask(message.text, message.images, message.files)
break
case "apiConfiguration":
if (message.apiConfiguration) {
@@ -296,33 +282,10 @@ export class Controller {
}
await this.postStateToWebview()
break
case "optionsResponse":
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message.text,
})
break
case "openInBrowser":
if (message.url) {
vscode.env.openExternal(vscode.Uri.parse(message.url))
}
break
case "showAccountViewClicked": {
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
break
}
case "fetchUserCreditsData": {
await this.fetchUserCreditsData()
break
}
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
await handleFileServiceRequest(this, "openFile", { value: mcpSettingsFilePath })
}
break
}
case "fetchMcpMarketplace": {
await this.fetchMcpMarketplace(message.bool)
break
@@ -380,32 +343,10 @@ export class Controller {
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
}
case "fetchLatestMcpServersFromHub": {
this.mcpHub?.sendLatestMcpServers()
break
}
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
"workbench.action.openSettings",
`@ext:saoudrizwan.claude-dev ${settingsFilter}`.trim(), // trim whitespace if no settings filter
)
break
}
case "invoke": {
if (message.text) {
await this.postMessageToWebview({
type: "invoke",
invoke: message.text as Invoke,
})
}
break
}
// telemetry
case "telemetrySetting": {
if (message.telemetrySetting) {
@@ -468,11 +409,9 @@ export class Controller {
if (answer === "Delete All Except Favorites") {
await this.deleteNonFavoriteTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
} else if (answer === "Delete Everything") {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
}
this.postMessageToWebview({ type: "relinquishControl" })
break
@@ -489,6 +428,13 @@ 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)
@@ -655,12 +601,12 @@ export class Controller {
if (this.task.isAwaitingPlanResponse && didSwitchToActMode) {
this.task.didRespondToPlanAskBySwitchingMode = true
// Use chatContent if provided, otherwise use default message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
images: chatContent?.images,
})
await this.task.handleWebviewAskResponse(
"messageResponse",
chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
chatContent?.images || [],
chatContent?.files || [],
)
} else {
this.cancelTask()
}
@@ -691,7 +637,7 @@ export class Controller {
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
this.task.abandoned = true
}
await this.initTask(undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
await this.initTask(undefined, undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
}
}
@@ -735,10 +681,7 @@ export class Controller {
await storeSecret(this.context, "clineApiKey", apiKey)
// Send custom token to webview for Firebase auth
await this.postMessageToWebview({
type: "authCallback",
customToken,
})
await sendAuthCallbackEvent(customToken)
const clineProvider: ApiProvider = "cline"
await updateGlobalState(this.context, "apiProvider", clineProvider)
@@ -810,6 +753,7 @@ export class Controller {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
headers: {
"Content-Type": "application/json",
"User-Agent": "cline-vscode-extension",
},
})
@@ -971,10 +915,7 @@ export class Controller {
input += `\nProblems:\n${problemsString}`
}
await this.postMessageToWebview({
type: "addToInput",
text: input,
})
await sendAddToInputEvent(input)
console.log("addSelectedCodeToChat", code, filePath, languageId)
}
@@ -992,10 +933,7 @@ export class Controller {
// terminalName
// })
await this.postMessageToWebview({
type: "addToInput",
text: `Terminal output:\n\`\`\`\n${output}\n\`\`\``,
})
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${output}\n\`\`\``)
console.log("addSelectedTerminalOutputToChat", output, terminalName)
}
@@ -1084,7 +1022,7 @@ export class Controller {
if (id !== this.task?.taskId) {
// non-current task
const { historyItem } = await this.getTaskWithId(id)
await this.initTask(undefined, undefined, historyItem) // clears existing task
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
}
await this.postMessageToWebview({
type: "action",
@@ -1156,19 +1094,6 @@ export class Controller {
await this.postStateToWebview()
}
async refreshTotalTasksSize() {
getTotalTasksSize(this.context.globalStorageUri.fsPath)
.then((newTotalSize) => {
this.postMessageToWebview({
type: "totalTasksSize",
totalTasksSize: newTotalSize,
})
})
.catch((error) => {
console.error("Error calculating total tasks size:", error)
})
}
async deleteTaskWithId(id: string) {
console.info("deleteTaskWithId: ", id)
@@ -1211,7 +1136,7 @@ export class Controller {
console.debug(`Error deleting task:`, error)
}
this.refreshTotalTasksSize()
await this.postStateToWebview()
}
async deleteTaskFromState(id: string) {
@@ -1228,10 +1153,7 @@ export class Controller {
async postStateToWebview() {
const state = await this.getStateToPostToWebview()
// For testing: Bypass gRPC stream and send state directly
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
await this.postMessageToWebview({ type: "state", state: state })
// await sendStateUpdate(state) // Original line for the GrPC stream
await sendStateUpdate(state)
}
async getStateToPostToWebview(): Promise<ExtensionState> {
@@ -1287,7 +1209,7 @@ export class Controller {
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
vscMachineId: vscode.env.machineId,
distinctId: telemetryService.distinctId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
@@ -1,5 +1,6 @@
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
import type { AddRemoteMcpServerRequest } from "../../../shared/proto/mcp"
import { McpServers } from "../../../shared/proto/mcp"
import type { Controller } from "../index"
/**
@@ -23,7 +24,7 @@ export async function addRemoteMcpServer(controller: Controller, request: AddRem
const protoServers = convertMcpServersToProtoMcpServers(servers)
return { mcpServers: protoServers }
return McpServers.create({ mcpServers: protoServers })
} catch (error) {
console.error(`Failed to add remote MCP server ${request.serverName}:`, error)
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Controller } from "../index"
import type { McpServers } from "../../../shared/proto/mcp"
import { McpServers } from "../../../shared/proto/mcp"
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
import { StringRequest } from "@/shared/proto/common"
@@ -17,7 +17,7 @@ export async function deleteMcpServer(controller: Controller, request: StringReq
// Convert application types to protobuf types
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
return { mcpServers: protoServers }
return McpServers.create({ mcpServers: protoServers })
} catch (error) {
console.error(`Failed to delete MCP server: ${error}`)
throw error
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create mcp service registry
const mcpService = createServiceRegistry("mcp")
// Export the method handler types and registration function
export type McpMethodHandler = ServiceMethodHandler
export type McpStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = mcpService.registerMethod
// Export the request handlers
export const handleMcpServiceRequest = mcpService.handleRequest
export const handleMcpServiceStreamingRequest = mcpService.handleStreamingRequest
export const isStreamingMethod = mcpService.isStreamingMethod
// Register all mcp methods
registerAllMethods()
-26
View File
@@ -1,26 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { deleteMcpServer } from "./deleteMcpServer"
import { downloadMcp } from "./downloadMcp"
import { refreshMcpMarketplace } from "./refreshMcpMarketplace"
import { restartMcpServer } from "./restartMcpServer"
import { toggleMcpServer } from "./toggleMcpServer"
import { toggleToolAutoApprove } from "./toggleToolAutoApprove"
import { updateMcpTimeout } from "./updateMcpTimeout"
// Register all mcp service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("deleteMcpServer", deleteMcpServer)
registerMethod("downloadMcp", downloadMcp)
registerMethod("refreshMcpMarketplace", refreshMcpMarketplace)
registerMethod("restartMcpServer", restartMcpServer)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("toggleToolAutoApprove", toggleToolAutoApprove)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
@@ -0,0 +1,17 @@
import { Controller } from ".."
import { Empty, EmptyRequest } from "@shared/proto/common"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
/**
* Opens the MCP settings file in the editor
* @param controller The controller instance
* @param _request Empty request
* @returns Empty response
*/
export async function openMcpSettings(controller: Controller, _request: EmptyRequest): Promise<Empty> {
const mcpSettingsFilePath = await controller.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
await openFileIntegration(mcpSettingsFilePath)
}
return Empty.create()
}
@@ -1,5 +1,5 @@
import type { EmptyRequest } from "../../../shared/proto/common"
import type { McpMarketplaceCatalog } from "../../../shared/proto/mcp"
import { McpMarketplaceCatalog } from "../../../shared/proto/mcp"
import type { Controller } from "../index"
/**
@@ -19,9 +19,9 @@ export async function refreshMcpMarketplace(controller: Controller, _request: Em
}
// Return empty catalog if nothing was fetched
return { items: [] }
return McpMarketplaceCatalog.create({ items: [] })
} catch (error) {
console.error("Failed to refresh MCP marketplace:", error)
return { items: [] }
return McpMarketplaceCatalog.create({ items: [] })
}
}

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