Compare commits

...

52 Commits

Author SHA1 Message Date
celestial-vault 7c91440b15 use optimized e2e script 2025-07-19 12:50:20 -07:00
celestial-vault e82886d3a7 empty commit 2025-07-19 11:21:31 -07:00
celestial-vault 20239c5edd change cache path to check for vscode 2025-07-19 11:15:07 -07:00
celestial-vault 875cac4d4b remove extra logs 2025-07-19 11:03:13 -07:00
celestial-vault 74fef93b2c cache vscode and playwright downloads 2025-07-19 10:53:09 -07:00
Ara be2d416359 Updating Mintlify version (#5030)
* Updating Mintlify version

* clean up and ignore docs workspace for lint (#5032)

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-07-18 19:29:28 -07:00
celestial-vault 42ffc30324 Separate plan act model settings (#4827)
* add provider handler options types and only pass those fields when building

* use separate fields for plan and act mode for ephemeral model settings

* post merge issues fixed: bedrock and cline api build handlers; current mode reading in controller/index.ts; adding a couple fields to welcomeView migration

* add in moonshotApiKey to settings-conversion after main merge

* fix type errors

* fix frontend types

* fix code after merge conflicts; switch groq provider to plan/act paradigm

* consolidate to Mode type

* remove promise resolve

* move huggingface provider into the new schema

* add new providers to migration function

* use normalizeApiConfiguration for settings menu provider value to avoid undefined state error
2025-07-18 14:14:31 -07:00
Sarah Fortune b0c67e9f83 Fix clean up part of the build-proto script (#5027) 2025-07-18 13:30:04 -07:00
Sarah Fortune fd366208a1 Fix the structure for the hosts package (#5020)
* Fix the structure for the hosts package.

Right now we support two host platforms: vscode using the internal host bridge service (inside the same process), and other platforms using the external host bridge (using gRPC over local sockets).

The structure of the two packages should mirror each other

```
hosts/
  vscode/
    VscodeDiffViewProvider
    VscodeWebViewProvider
    etc ...
  external/
    ExternalDiffViewProvider
    ExternalWebViewProvider
    etc ...
```

* Fix imports

* Fix imports

* Fix imports
2025-07-18 13:22:23 -07:00
Sarah Fortune 6c7bc58215 Move remaining platform specific code out of the diff view provider. (#5009)
* .

* Move the DecorationController into the hosts/vscode package.
2025-07-18 13:16:19 -07:00
Sarah Fortune f69a378ff4 Add eslint checks for more vscode API calls (#5023)
* Add eslint check for more vscode API calls

Add eslint checks to prevent vscode API calls from being reintroduced after they were switched to the host bridge.

The ones that are not enabled are not completely migrated, so they are not turned on because it would cause too many warnings, and make developers used to ignoring them.

* Update eslint-rules/no-direct-vscode-api.js

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

* Update warning message text

* Move the check for if the file is being included in the linter check or not into its own function

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-18 13:08:13 -07:00
Sarah Fortune 309e3bd85c Fix import in McpHub (#5021) 2025-07-18 11:59:07 -07:00
Daniel Campos Olivares eb6eb371e4 fix: Add documentation to nav menu (#5014) 2025-07-18 11:25:40 -07:00
Mohan Raj Rajamanickam 419e3e4677 fix: mcp servers are not started when disabled (#4501)
* fix: disabled servers should not be started

* refactor to dedupe

* add changeset

* Update moody-crabs-relate.md

* minor comment tweaks

* use unknown instead of any
2025-07-18 10:35:19 -07:00
Sarah Fortune e95eecd65f Just shut up already (#5011)
Don't display a notification when the MCP servers have updated.
2025-07-17 23:32:10 -07:00
Sarah Fortune e83e71cc6d Remove class that is unused, and other class that is totally commented out. (#5010) 2025-07-17 23:00:29 -07:00
Sarah Fortune a9e526e99b Move save() and getDocument() to the platform specific diff classes (#5008)
* Move logic to save the diff document and get the diff content to the platform specific diff view providers.

Add getDocumentText to the diff service.

The ExternalDiffViewProvider should be using its activeDiffEditorId (not the activeDiffEditor, which is the vscode Editor object)

* Check activeDiffEditorId

* Update src/integrations/editor/DiffViewProvider.ts

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

* Use await with this.saveDocument()

* Remove test file

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-17 18:06:54 -10:00
Sarah Fortune 72f16a8c30 Don't need to use Message.create({...}) with the host bridge (#4999)
* Use await when calling async function showMessage.

Use await when calling `windowClient.showMessage`, otherwise the caller doesn't wait for the request to complete and any exceptions are lost.

For the host bridge RPCs, you don't need to do `ShowMessageRequest.create({...})`, you can just pass the request directly like: `{...}. The Message.create() was needed for the ProtoBus because of the way it was implemented, it couldn't be type-checked by the compiled. It's not needed elsewhere.

* Dont use await, because it blocks until the message is dismissed

* Dont need `?.selectedOption`, the response cannot be null

* Dont use await
2025-07-17 20:15:42 -07:00
Toshii 1a3ec9f024 move environment setting to launch & publish (#5002)
* move env setting to launch & publish

* change env name

* Update webview-ui/vite.config.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* update env location

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-07-17 18:49:36 -07:00
Sarah Fortune b3fa3ad0d3 Add new RPCs to the host bridge diff service (#4997)
* Add truncate document to the hostbridge diff service.

# Conflicts:
#	src/standalone/ExternalDiffviewProvider.ts

* Fix typo

* Add host bridge rpcs for the diff service

* Add saveDocument and closeDiff to the host bridge.

* Formatting

* Use the host bridge in ExternalDiffviewProvider

* Make comments less verbose
2025-07-17 18:36:43 -07:00
github-actions[bot] 45241fcccf v3.19.7 Release Notes (#4983)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.19.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: pashpashpash <nik@cline.bot>
2025-07-17 18:19:36 -07:00
pashpashpash 2a3f0e9418 Adding huggingface provider (#4952)
Co-authored-by: arafatkatze <arafat.da.khan@gmail.com>
2025-07-17 18:00:28 -07:00
Toshii 4334764903 remove warning (#5003) 2025-07-17 17:53:25 -07:00
Bee b3a10243b8 Unify error handling and display logic (#4984)
* Unify error handling and display

Several improvements to error handling and reporting within the Cline extension, focusing on providing more informative error messages to the user and improving telemetry data.

- **Error Handling:**
  - Introduces a `ClineError` class to encapsulate Cline-specific errors, providing structured data about the error (status code, request ID, etc.).
  - The `ErrorService` now creates and logs `ClineError` instances, improving error reporting to Sentry and telemetry.
  - Removes redundant error formatting logic from `src/core/task/utils.ts`, relying on the `ClineError` class for consistent error representation.
  - The Cline API handler now throws the raw error, allowing the `ClineError` class to handle the error formatting.

- **UI Improvements:**
  - Introduces new UI components (`ErrorRow`, `ErrorBlockTitle`) to display error messages in a more user-friendly format within the chat interface.
  - Displays credit limit errors with detailed information and a link to purchase credits.
  - Improves the display of rate limit errors and authentication errors.
  - Adds specific handling for PowerShell-related errors, providing a link to a troubleshooting guide.

- **Telemetry:**
  - Captures provider API errors using `ClineError` data, providing more detailed information about the error in telemetry reports.

- **Other Changes:**
  - Fixes an import path in `src/services/posthog/PostHogClientProvider.ts`.
  - Adds tests for the new UI components.
  - Adds `error` field to `ClineMessage` to transport `ClineError` instances to the webview.

These changes aim to provide a better user experience by displaying more informative error messages and improving the overall reliability of the Cline extension.

* include provider id

* includes model and provider id

* fix test

* return gracefully for empty response
2025-07-17 16:21:18 -07:00
Bee 8e95c136a6 Handle auth state changes in all extension windows (#4987)
* Handle auth state changes in all extension windows

Addresses an issue where authentication state changes (login/logout) were not being properly propagated across all extension windows.

The `onDidChange` event handler for `clineAccountId` secret now checks if the secret was added/updated (login) or removed (logout).

- If the secret exists, `restoreRefreshTokenAndRetrieveAuthInfo` is called to restore auth info (login from another window).
- If the secret is removed, `handleDeauth` is called to handle logout for all windows.

This ensures that all extension windows are kept in sync with the current authentication state.

* changeset
2025-07-17 15:40:30 -07:00
Daniel Campos Olivares 0b66faa1dd chore: Introduce SAP AI Core documentation (#4961)
* feat: Introduce SAP AI Core documentation

* fix: Markdown lint

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>

* fix: Typo

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>

---------

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>
2025-07-17 14:10:30 -07:00
Sarah Fortune 3d2dc1c5c4 Move platform specific code out of the DiffViewProvider and into the VscodeDiffViewProvider (#4980)
* Add a way to reset state for the platform specific diff view providers.

Add the abstract method `resetDiffView()` to the DiffViewProvider.

On Vscode it reset the active diff editor and decorations.

On External diff view providers, it reset the diff editor ID.

* Add a way to truncate the document to the platform specific diff view providers.

This is used when the last update is recieved to remove any content from the bottom of the document.

* Move `closeDiffView` to the platform specific diff view classes.

* Update comment

* Use await for async function

* Move setting the cursor position into replaceText
2025-07-17 12:35:48 -07:00
Andrei Eternal 028412579b Standalone Terminal Manager & remove other vscode-impls (now covered by host bridge) (#4993)
* Revert "experimental vscode impls & build-proto cleanup (#4493)"

This reverts commit f00c5f4ecc.

* leave terminal impls

* terminal manager switch for standalone

* Export standaloneTerminalManager to global

* clean up standalone terminal switch

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-07-17 12:11:45 -07:00
Sarah Fortune f04788c2ec Move the logic to scroll the diff editor window into the platform specific classes (#4977)
* Move the logic to scroll the diff editor window into the platform specific classes.

Move the current logic into the VscodeDiffViewProvider.

The ExternalDiffViewProvider will use the host bridge to scroll the editor tab. But for now it doesn't do anything.

* Add comment

* Remove log messages
2025-07-17 11:26:40 -07:00
Toshii 5fba39f886 move to simple config for setting env details of backend (#4978)
* backend env

* frontend env config

* rename config

* preview update

* separate mcp link

* mcp url updated

* config change to local

* webview config to local
2025-07-16 22:53:08 -07:00
Toshii 9a4e3655b3 remove import (#4981) 2025-07-16 19:40:43 -07:00
Tomás Barreiro b18ad77539 Improve Claude Code errors and create Docs (#4968)
* Create docs

* Improve the Claude Code error messages

* Replace with remote image
2025-07-16 18:22:01 -07:00
github-actions[bot] c50fce8101 v3.19.6 Release Notes (#4976)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.19.6

* more modifications

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-16 17:21:22 -07:00
Sarah Fortune 47f84d7c2c Improve generated code that registers the vscode host bridge handlers. (#4972) 2025-07-16 17:14:37 -07:00
Ara 17c57162cd Refactoring terminal process logic and edge cases for simplicity (#4673)
* Refactor terminal output capture for better reliability

- Extract emitCurrentTerminalContents() as a private method for reusability
- Improve handling of commands with no/delayed output by capturing actual terminal contents instead of generic messages
- Add multiple fallback attempts (100ms, 1s, 3s) for terminals without shell integration
- Enhance timeout comments to clarify the 3-second delay purpose
- Add .tool-versions file (likely for version management)

* Refactor TerminalProcess run method into smaller functions

Split the monolithic run() method into focused helper methods for better
maintainability and readability. Extracted initialization, shell integration
execution, stream processing, and cleanup logic into separate private methods.
This improves code organization without changing functionality.

* Fixing the handling of echo commands and changing the wording of the terminal capture line

* fix: Embracing the ffmpeg for opus support
2025-07-16 16:33:01 -07:00
Toshii 0f1f3d84f7 add auto refresh to accounts page (#4901)
* add auto refresh to accounts page

* update

* 10 sec
2025-07-16 16:14:18 -07:00
Saoud Rizwan 0c5832b7a2 Fix kimi k2 provider sorting (#4975)
* Fix kimi2 provider sorting

* Create smart-flies-eat.md
2025-07-16 16:11:12 -07:00
celestial-vault 11c4c58ed3 update the current task's consecutiveAutoApprovedRequestsCount on max request change (#4955) 2025-07-16 16:07:01 -07:00
pashpashpash 27bf78ddbd swapping latest diff algo as default (#4412)
* swapping latest diff algo as default

* updating

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-16 15:59:48 -07:00
pashpashpash c3161567d3 removing unneeded docs section (#4974) 2025-07-16 15:50:08 -07:00
pashpashpash 516d7bccc9 adding documentation workflow (#4598)
* adding documentation workflow

* Update .clinerules/workflows/writing-documentation.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* good example

* good example

* language

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-16 15:37:24 -07:00
pashpashpash e91c8208a3 fix streamlit dashboard to be compatible with both old and new versions of streamlit (#4922) 2025-07-16 15:28:02 -07:00
Sarah Fortune 0fb40527c8 Add a script to run the standalone service. (#4966)
* Add a script to run the standalone service.

Remove spammy log statements.

* Add comment

* Fix some vscode stubs

* Update scripts/runstandalone.sh

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

* Update scripts/runstandalone.sh

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

* Update scripts/runstandalone.sh

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

* Remove old script

* Add comment

* Add comment

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-16 15:12:52 -07:00
Sarah Fortune 8a2e90084d Change the port numbers for the ProtoBus service and HostBridge. (#4969)
Don't use the default gRPC port number because it's more likely to already be in use.
2025-07-16 11:28:07 -07:00
Sarah Fortune 4c3384988c open-file calls vscode.workspace.openTextDocument but doesn't use the result. (#4960) 2025-07-16 03:01:25 -07:00
Daniel Steigman 8539bdce24 Upgraded telemetry to capture each message turn separately (#4954)
* refactor: Ugraded telemetry to capture each message turn seperatly

* go back to the correct posthog key oops
2025-07-15 22:49:35 -07:00
Bee cd7f3ef6a7 Disable recording webview click events (#4709)
* Disable recording webview click events

Introduces a temporary measure to disable the recording of webview click events in PostHog. This is achieved by adding a `temporaryDisabled` flag that, when true, prevents the initialization of PostHog and stops the identification of users.

This change is intended to be temporary and should be reverted in a future commit by removing the `temporaryDisabled` flag.

* Use separate PostHog config for development environment

This commit introduces a separate PostHog project for the development environment. This allows us to track events in the development environment without polluting the production data.

The `posthogConfig` now uses `posthogDevEnvConfig` when `process.env.isDev` is true, and `posthogProdConfig` otherwise.

* process.env.IS_DEV

* Update src/shared/services/config/posthog-config.ts

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

* fix format

---------

Co-authored-by: Beatrix Woo <beatrix@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-15 22:46:58 -07:00
github-actions[bot] 80fbcda03b v3.19.5 Release Notes (#4941)
-   Add Groq as a new API provider with support for all Groq models including Kimi-K2
-   Add user role display in organization UI for Cline account users
-   Fix message dialogs not showing option buttons properly
-   Fix authentication issues when using multiple VSCode windows
2025-07-15 21:49:06 -07:00
pashpashpash cef79e06db not showing request id when insufficient balance (#4953)
* not showing request id when insufficient balance

* whoops

* betterrr
2025-07-15 21:38:11 -07:00
Ara 5dddbef65c Adding Groq provider (#4943) 2025-07-15 19:47:56 -07:00
Bee b6f6358d4a Set up E2E tests with Playwright (#4721)
* Add Playwright E2E tests

Adding end-to-end (E2E) testing capabilities using Playwright. It also updates the `@vscode/test-electron` dependency.

The changes include:

- Adding Playwright as a dev dependency.
- Adding `e2e` and `e2e:build` scripts to `package.json` for running E2E tests.
- Adding `@playwright/test` to the list of dependencies.
- Updating `@vscode/test-electron` from `2.4.1` to `2.5.2`.
- Adding `test-results` to `.gitignore` to exclude test result files.

* wip: github workflow

Adding a new GitHub Actions workflow for running end-to-end (E2E) tests using Playwright. The workflow is triggered on push to the main branch, pull requests, and manual workflow dispatch.

The workflow defines a matrix strategy to run tests on different runners (Ubuntu and Windows) and shards. It also uploads Playwright recordings as artifacts if the tests fail.

* add @vscode/vsce as dev dep

* update workflow

* apply feedback

* fix test workflows

* add command palette helper

This commit improves the reliability and efficiency of the end-to-end tests by:

- Adding a delay to the "Let's go!" button click in the auth test to ensure the action is properly registered.
- Adding an expectation to ensure the "Get Started for Free" button is no longer visible after API key submission.
- Caching the "Use your own API key" button to avoid redundant lookups.
- Introducing a `runCommandPalette` helper function to streamline command execution within the VS Code environment.
- Disabling notifications before running the tests to prevent interference.

* state change

* set TEMP_PROFILE

* v3.18.7 Release Notes

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.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: pashpashpash <nik@cline.bot>

* Remove optimistic loading from organization dropdown (#4746)

* update build script to javascript

* fix match

* Add mode switching to chat test

* expected

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
2025-07-16 07:29:58 +05:30
Bee e607d02ab2 Fix: pass items to showMessage in VS Code host bridge (#4949) 2025-07-15 18:14:30 -07:00
186 changed files with 12511 additions and 26937 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Display user role in organization UI
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: mcp servers are not started when disabled
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remove state parameter from auth callback link that allows redirect to work when multiple windows are opened
@@ -0,0 +1,392 @@
# General writing guide
# How I want you to write
I'm gonna write something technical.
It's often less about the nitty-gritty details of the tech stuff and more about learning something new or getting a solution handed to me on a silver platter.
Look, when I read, I want something out of it. So when I write, I gotta remember that my readers want something too. This whole piece? It's about cluing in anyone who writes for me, or wants me to write for them, on how I see this whole writing product thing.
I'm gonna lay out a checklist of stuff I'd like to have. It'll make the whole writing gig a bit smoother, you know?
## Crafting Compelling Titles
I often come across titles like "How to do X with Y,Z technology." These don't excite me because X or Y are usually unfamiliar unless they're already well-known. Its rarely the dream to use X unless X is the dream.
My dream isnt to use instructor, its to do something valueble with the data it extracts
An effective title should:
- Evoke an emotional response
- Highlight someone's goal
- Offer a dream or aspiration
- Challenge or comment on a belief
- Address someone's problems
I believe it's more impactful to write about specific problems. If this approach works, you can replicate it across various scenarios rather than staying too general.
- Time management for everyone can be a 15$ ebook
- Time management for executives is a 2000$ workshop
Aim for titles that answer questions you think everyone is asking, or address thoughts people have but can't quite articulate.
Instead of "How I do something" or "How to do something," frame it from the reader's perspective with "How you can do something." This makes the title more engaging. Just make sure the difference is advisory if the content is subjective. “How I made a million dollars” might be more reasonable than “How to make a million dollars” since you are the subject and the goal might be to share your story in hopes of helping others.
This approach ultimately trains the reader to have a stronger emotional connection to your content.
- "How I do X"
- "How You Can do X"
Between these two titles, it's obvious which one resonates more emotionally.
You can take it further by adding specific conditions. For instance, you could target a particular audience or set a timeframe:
- How to set up Braintrust
- How to set up Braintrust in 5 minutes
## NO adjectiives
I want you to almost always avoid adjectives and try to use evidence instead. Instead of saying "production ready," you can write something like "scaling this to 100 servers or 1 million documents per second." Numbers like that will tell you exactly what the specificity of your product is. If you have to use adjectives rather than evidence, you are probably making something up.
There's no reason to say something like "blazingly fast" unless those things are already known phrases.
Instead, say "200 times faster" or "30% faster." A 30% improvement in recommendation system speed is insane.
There's a 200 times performance improvement because we went from one programming language to another. It's just something that's a little bit more expected and understandable.
Another test that I really like using recently is tracking whether or not the statements you make can be:
- Visualized
- Proven false
- Said only by you
If you can nail all three, the claim you make will be more likely to resonate with an audience because only you can say it.
Earlier this year, I had an example where I embedded all of Wikipedia in 17 minutes with 20 bucks, and it got half a million views. All we posted was a video of me kicking off the job, and then you can see all the log lines go through. You see the number of containers go from 1 out of 50 to 50 out of 50.
It was easy to visualize and could have been proven false by being unreproducible. Lastly, Modal is the only company that could do that in such an effortless way, which made it unique.
## Keep It Digestible
- Aim for 5-minute reads
- Write at a Grade 10 reading level
- Break up long paragraphs
- Use headers and bullet points
## Make It Scannable
- Bold key points
- Use subheadings every 3-4 paragraphs
- Include plenty of white space
- Add relevant examples
This structure works whether you're writing a tweet thread or a full blog post. The key is making complex ideas accessible.
# Guide to Writing Cline Documentation
## Some general principles for explaining features
If you're talking about a feature, it's helpful to start with a human-readable explanations that cover what the feature is in simple terms. Skip jargon and explain it like you're talking to someone who's never seen it before. This sets the foundation for everything that follows.
Combine location and usage into one flowing section. Tell users exactly where to find the feature and how to use it, but weave the instructions into natural prose with a good balance of bullet points, numbered lists, code examples (if applicable), mintlify components, and headers/subheaders. Users shouldn't have to jump between separate "where is it" and "how do I use it" sections.
Show the feature in action with real examples like actual files, workflows, or code. Users need to see concrete implementations, not just abstract descriptions. This is where understanding turns into practical knowledge.
When talking about a feature, include an inspiration section that sparks imagination. This section pushes people from understanding to action by showing them what becomes possible when they use this feature creatively. It's what separates good documentation from great documentation.
## Writing Principles That Actually Work
### Write for Action, Not Just Understanding
Documentation should motivate users to try things. Instead of just explaining how something works, focus on what users can accomplish with it. The inspiration section is crucial - it's what transforms passive readers into active users.
### Create a Natural Story Flow
It should feel like a conversation that naturally progresses from "what is this?" to "how do I use it?" to "here's a real example" to "imagine what you could do with this."
### Show Real Examples, Not Toy Demos
Provide actual workflow files, real code snippets, and concrete implementations that users can copy and adapt. Abstract examples don't help anyone - users want to see exactly what they'll be working with.
### Keep It Scannable But Not Fragmented
Write in prose that flows naturally when read completely, but structure it so users can quickly find specific information when they're troubleshooting. Avoid dense walls of text, but also avoid over-formatting with excessive bullet points and bold headers. There should be a nice visual heirarchy of balance between all elements, so you can quickly scan the page and find what you're looking for.
## Language and Tone Guidelines
Write clearly without dumbing things down. Use simple language when possible, but don't avoid technical terms that users need to know. Explain concepts in terms of what users can achieve rather than how the software works internally.
Make your writing conversational and encouraging. Phrases like "you can also try" or "when that works" feel more natural than rigid instructional language. Help users feel confident about trying new things.
Keep content concise and purposeful. Every sentence should either help users understand something or help them do something. If it doesn't serve one of those purposes, cut it.
Build in context and reasoning. Users want to understand why they're doing something, not just what to do. This builds confidence and helps them troubleshoot when things don't work exactly as expected.
## Practical Implementation
Structure each feature page consistently with the four-section approach, but let the content flow naturally within that structure. Use visual assets like videos and screenshots to complement the written content - they often communicate more effectively than paragraphs of description.
Link generously to related resources, examples, and deeper documentation. Users should never feel stuck or wonder where to go next. Maintain a repository of real examples that users can reference and adapt to their own needs.
The goal is documentation that feels more like helpful guidance from an experienced colleague than a technical manual. Users should finish reading feeling excited about what they can accomplish, not just informed about what the feature does.
## Balance Structure with Flexibility
While they discuss having consistent documentation structure, there's also mention of making content feel less rigid and more natural. The writing should follow guidelines while still feeling conversational and engaging.
## Bad examples
I personally hate this pattern of bullet point **Bold Text** colon and then more text:
<bad_example_of_writing>
#### macOS
1. **Switch to bash**: Go to Cline Settings → Terminal → Default Terminal Profile → Select "bash"
2. **Disable Oh-My-Zsh temporarily**: If using zsh, try `mv ~/.zshrc ~/.zshrc.backup` and restart VSCode
3. **Set environment**: Add to your shell config: `export TERM=xterm-256color`
#### Windows
1. **Use PowerShell 7**: Install from Microsoft Store, then select it in Cline settings
2. **Disable Windows ConPTY**: VSCode Settings → Terminal Integrated: Windows Enable Conpty → Uncheck
3. **Try Command Prompt**: Sometimes simpler is better - switch to cmd.exe
#### Linux
1. **Use bash**: Most reliable option - select in Cline settings
2. **Check permissions**: Ensure VSCode has terminal access permissions
3. **Disable custom prompts**: Comment out prompt customizations in `.bashrc`
</bad_example_of_writing>
We should instead strive to write beautiful docs that read well. We can use bullet points and numbered lists but it should read naturally and be delightful to look at hierachally when scanning through the doc. There should be a good balance between blocks of text, code snippets, paragraphs, numbered lists, and bullet points. When scanning the documentation visually, you should feel like you're adminiring a tasteful art piece.
<good_example_of_writing>
#### macOS
The most common fix is switching to bash. Navigate to Cline Settings → Terminal → Default Terminal Profile and select "bash" from the dropdown.
If you're still having issues, Oh-My-Zsh might be interfering with terminal integration. Try temporarily disabling it:
- Run `mv ~/.zshrc ~/.zshrc.backup`
- Restart VSCode
You can also add `export TERM=xterm-256color` to your shell configuration file to improve compatibility.
#### Windows
PowerShell 7 provides the most reliable experience. Install it from the Microsoft Store, then select it in your Cline settings.
Still seeing problems? Try these solutions:
- Disable Windows ConPTY: VSCode Settings → Terminal Integrated: Windows Enable Conpty → uncheck
- Switch to Command Prompt (cmd.exe) - sometimes simpler shells work better
#### Linux
Bash is your most dependable option. Select it in Cline settings if you haven't already.
Check these common issues:
- Ensure VSCode has terminal access permissions
- Temporarily comment out custom prompt configurations in your `.bashrc`
</good_example_of_writing>
This is much more natural to read. Writing this way creates a conversational flow, and bullet points are used idiomatically.
# Using Mintlify Components Idiomatically
Mintlify's custom components can transform basic documentation into engaging, scannable content that users actually want to read. Here's how to use them effectively.
## Visual Content with Frames
Videos and images should be wrapped in `<Frame>` components rather than using raw HTML or markdown. This creates consistent styling and proper responsive behavior.
For videos, embed them directly rather than linking externally. Users are much more likely to watch a 30-second demonstration than click through to another platform:
```jsx
<Frame>
<iframe
style={{ width: "100%", aspectRatio: "16/9" }}
src="https://www.youtube.com/embed/your-video-id"
title="Feature demonstration"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
</Frame>
```
Screenshots work similarly - the frame provides visual polish and consistency:
```jsx
<Frame>
<img src="/path/to/screenshot.png" alt="Descriptive alt text" />
</Frame>
```
## Cards for Navigation and Overview
Cards excel at creating scannable overviews that link to detailed documentation. They're perfect for feature listings, getting started guides, or any section where users need to choose their path.
Use the two-column layout for related features:
```jsx
<Columns cols={2}>
<Card title="Feature Name" icon="relevant-icon" href="/link/to/docs">
Brief description that explains what this feature does and why someone would use it.
</Card>
<Card title="Related Feature" icon="another-icon" href="/another/link">
Another concise explanation that helps users understand the value proposition.
</Card>
</Columns>
```
The key is writing card descriptions that are informative enough to help users decide whether to click through, but concise enough to scan quickly. Each card should answer "what does this do?" and "why would I need this?"
## Tips and Notes for Context
Use `<Tip>` components for helpful information that enhances the main content without cluttering it:
```jsx
<Tip>
Pro tip: You can combine multiple @ mentions in a single message to give Cline
comprehensive context about your issue.
</Tip>
```
`<Note>` components work well for important caveats or technical limitations:
```jsx
<Note>
Due to VS Code limitations, some features require specific settings to work properly.
</Note>
```
`<Info>` is also cool:
<Info>
**Quick Fix**: If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings.
This resolves 90% of terminal integration problems.
</Info>
**Never** fall into that awful **Bold Text** - description pattern that we specifically identified as bad writing. The content should flow naturally as connected thoughts rather than feeling like a templated AI response with forced formatting.
## When to Use Bullet Points and Numbered Lists Strategically
Bullet points serve functional purposes - use them for:
**Sequential actions or troubleshooting steps** where users need to follow a specific order:
1. Install the extension
2. Restart VSCode
3. Check the settings panel
**Lists of related options** where users need to choose one approach:
- Try PowerShell 7 for the most reliable experience
- Switch to Command Prompt if you're still having issues
- Use WSL Bash for Linux compatibility
**Quick reference items** that users might need to scan quickly when problem-solving.
**Improving Visual Hierarchy** when there's a wall of text - that's a good time to introduce bullet points or numbered lists.
Each bulleted item or numbered list should be a discrete action or piece of information that benefits from being visually separated. This is a key weapon you can employ when going for that artwork experience I mentioned earlier.
<good_example_of_bullet_points>
## Finding and Configuring Terminal Settings
You can access Cline's terminal settings by clicking the settings icon in the Cline sidebar, then navigating to the Terminal section. These settings control how Cline interacts with your system's terminal.
- The **Default Terminal Profile** setting determines which shell Cline uses for executing commands. If you're experiencing issues, this is usually the first thing to change. I personally keep this set to `bash` on all my systems because it's the most reliable option, even though I use `zsh` for my regular terminal work.
- **Shell Integration Timeout** controls how long Cline waits for the terminal to become ready. The default is 4 seconds, but if you have a heavy shell configuration (lots of plugins, slow startup scripts), you might need to increase this to 10 or even 15 seconds. I've found that WSL environments and SSH connections often need longer timeouts.
- The **Enable Aggressive Terminal Reuse** setting determines whether Cline tries to reuse existing terminals even when they're not in the correct directory. When this causes problems (commands running in the wrong directory, virtual environment issues), disabling it creates more terminal instances but ensures each command runs in a clean state.
- **Terminal Output Line Limit** sets how many lines of output Cline will read from commands. The default of 500 lines works for most cases, but you might need to adjust it. For verbose build outputs, increase it. For commands with progress bars that spam thousands of lines, decrease it to around 100 to avoid consuming too many tokens.
</good_example_of_bullet_points>
## Write Like a Human, Not an AI
Keep it short and direct. If you can say something in fewer words, do it. Long explanations often confuse more than they help.
Use normal sentence lengths. Mix short and long sentences naturally, like you would when talking to someone. Avoid those meandering compound sentences that go on forever.
Cut the corporate speak. Instead of "utilize," say "use." Instead of "in order to," just say "to." Write like you're explaining something to a colleague, not drafting a press release.
Don't over-explain obvious things. If users are reading documentation, they probably understand basic concepts. Respect their intelligence.
## Never use em dashes or emojis
That's pretty much it. Never use them. Only AI writes with em dashes or emojis.
# Anthropomorphizing Cline
When referring to Cline, always call him a "him" not an "it".
Bad example:
- When Cline cant execute commands or read their output, you lose access to one of its most powerful capabilities.
Good Example:
- When Cline cant execute commands or read their output, you lose access to one of his most powerful capabilities.
# Using "I" when sharing your workflow
Adding a personal touch goes a long way. There are great examples in the docs currently where I use "I" to share how I personally use cline, from dev to dev. It's a great technique.
# Crosslinking relevant documentation pages
Make sure you crosslink when you're done writing the docs. If there are relevant docs, just link to them.
# Brevity is the soul of wit
Don't ramble if you don't need to. Use bullet points and numbered lists. Keep things easy to read.
<bad_example>
When Cline can't execute commands or read their output, you lose access to one of his most powerful capabilities. Terminal integration problems are frustrating, but they're usually fixable with a few simple changes.
## The Most Common Problem: Shell Integration Issues
If you're seeing "Shell integration unavailable" or Cline isn't getting command output, the issue is almost always your shell configuration. Complex shell setups with custom prompts, plugins, and fancy configurations can interfere with VSCode's terminal integration.
**Switch to bash first.** This fixes the problem 90% of the time. Navigate to Cline Settings → Terminal → Default Terminal Profile and select "bash" from the dropdown. Restart VSCode after making this change.
Still having issues? Try increasing the shell integration timeout. Go to Cline Settings → Terminal → Shell Integration Timeout and change it from 4 seconds to 10 seconds. Heavy shell configurations need more time to initialize properly.
If commands are running in the wrong directories or you're seeing weird behavior, disable aggressive terminal reuse. In Cline Settings → Terminal, uncheck "Enable aggressive terminal reuse." This creates more terminal instances but ensures each command runs in a clean environment.
</bad_exaxmple>
The first part is total filler, useless to any serious developer. You can tell it's written by a non technical person that doesn't value clean, straightforward information.
<good_example>
## Shell Integration Issues
If you're seeing "Shell integration unavailable" or Cline can't read command output, your shell configuration is interfering with VSCode's terminal integration.
**Switch to bash first.** Go to Cline Settings → Terminal → Default Terminal Profile and select "bash." This fixes 90% of problems.
Still broken? Try these:
- Increase shell integration timeout to 10 seconds in Cline Settings → Terminal
- Disable "aggressive terminal reuse" if commands run in wrong directories
- Restart VSCode after making changes
</good_example>
The good version cuts straight to the problem and solution. No hand-holding, no emotional language about frustration, just the facts: what's wrong, how to fix it, what to try next. Respects that developers want information, not sympathy.RetryClaude can make mistakes. Please double-check responses.
ALWAYS consider your audience. And your audience is devs who don't want their time wasted. Give them the info. I cannot stress this enough. Use bullet points and numbered lists. Prose is good, but every word should actually mean something to the dev reading it.
# Lastly, before you start writing docs
1. Internalize these guidelines. I mean it.
2. Read `docs/docs.json` and get an understanding of the structure of the docs. This will come in handly at the end when you're doing a final pass so you can cross link to docs where relevant.
3. Read some good examples that I personally wrote and am proud of:
- docs/features/slash-commands/workflows.mdx
- docs/features/slash-commands/new-task.mdx
- docs/features/at-mentions/overview.mdx
- docs/features/drag-and-drop.mdx
4. If the user specifies any other instructions make sure you follow them.
+108
View File
@@ -0,0 +1,108 @@
name: E2E Tests
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
matrix_prep:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
e2e:
needs: matrix_prep
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
runs-on: ${{ matrix.runner }}-latest
timeout-minutes: 20
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
# Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
# Run optimized E2E tests (eliminates redundant builds)
- name: Run E2E tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:e2e:optimal
- name: Run E2E tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:e2e:optimal
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: playwright-recordings-${{ matrix.runner }}
path: |
test-results/playwright/
+1
View File
@@ -94,6 +94,7 @@ jobs:
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
CLINE_ENVIRONMENT: production
run: |
# Required to generate the .vsix
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
+4
View File
@@ -68,6 +68,10 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Install local modules on windows
if: runner.os == 'Windows' && steps.root-cache.outputs.cache-hit == 'true'
run: |
+4 -6
View File
@@ -22,16 +22,14 @@ coverage
*evals.env
# Generated files
## Generated files ##
src/generated/
# Core
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
# Host bridge
src/hosts/vscode/client/host-grpc-client.ts
# E2E Tests
test-results
+3 -1
View File
@@ -5,4 +5,6 @@ webview-ui/build/
package-lock.json
src/core/prompts/system.ts
src/core/prompts/model_prompts/claude4.ts
evals/
evals/
docs/
out/
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "@vscode/test-cli"
import path from "path"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js}",
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
+4 -2
View File
@@ -14,7 +14,8 @@
"preLaunchTask": "${defaultBuildTask}",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
{
@@ -37,7 +38,8 @@
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
{
+21 -4
View File
@@ -1,15 +1,16 @@
# Default
.vscode/**
.vscode-test/**
out/**
dist-standalone/**
node_modules/**
out/
dist-standalone/
node_modules/
src/**
standalone/**
.gitignore
.yarnrc
esbuild.js
**/tsconfig.json
vsc-extension-quickstart.md
tsconfig*.json
**/.eslintrc.json
**/*.map
**/*.ts
@@ -23,6 +24,17 @@ eslint-rules/**
.nvmrc
.gitattributes
.prettierignore
.husky/
.github/
eslint-rules/
old_docs/
evals/
.changie.yaml
.codespellrc
.mocharc.json
buf.yaml
.changeset/
.clinerules/
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
@@ -47,3 +59,8 @@ old_docs/**
# Include icons
!assets/icons/**
# Ignore E2E build files
e2e-build.js
e2e.vsix
test-results/
+19
View File
@@ -1,5 +1,24 @@
# Changelog
## [3.19.7]
- Add Hugging Face as a new API provider with support for their inference API models
- Improve Claude Code error messages with better guidance for common setup issues (Thanks @BarreiroT!)
- Fix authentication sync issues when using multiple VSCode windows
## [3.19.6]
- Improve Kimi K2 model provider routing with additional provider options for better availability and performance
- Fixed terminal bug where Cline failed to capture output of certain fast-running commands
- Fixed bug with increasing auto approved number of requests not resetting the counter mid-task
## [3.19.5]
- Add Groq as a new API provider with support for all Groq models including Kimi-K2
- Add user role display in organization UI for Cline account users
- Fix message dialogs not showing option buttons properly
- Fix authentication issues when using multiple VSCode windows
## [3.19.4]
- Add ability to choose Chinese endpoint for Moonshot provider
+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, 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.
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. 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.
+2 -2
View File
@@ -61,7 +61,6 @@
"getting-started/installing-cline",
"getting-started/installing-dev-essentials",
"getting-started/model-selection-guide",
"getting-started/our-favorite-tech-stack",
"getting-started/task-management",
"getting-started/understanding-context-management",
"getting-started/what-is-cline"
@@ -160,7 +159,8 @@
"provider-config/openai",
"provider-config/openai-compatible",
"provider-config/openrouter",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/sap-aicore"
]
},
{
@@ -1,238 +0,0 @@
---
title: "Our Favorite Tech Stack"
description: "A curated list of our recommended technologies and tools for building modern web applications with Cline."
---
## Recommended Stack for New Cline Users (2025)
### Your Complete Development Environment
#### Development Tools
- **VS Code** - Your code editor, [download here](https://code.visualstudio.com/)
- **GitHub** - Where your code lives, [sign up here](https://github.com)
#### Frontend
- **Next.js 14+** - React framework with App Router
- **Tailwind CSS** - Beautiful styling without writing CSS
- **TypeScript** - JavaScript, but safer and smarter
#### Backend
- **Supabase** - Your complete backend solution, [sign up with GitHub](https://supabase.com)
- PostgreSQL database
- Authentication
- File storage
- Real-time updates
#### Deployment
- **Vercel** - Where your app runs, [sign up with GitHub](https://vercel.com)
- Automatic deployments from GitHub
- Preview deployments for testing
- Production-ready CDN
#### AI Development
Choose your AI assistant based on your needs:
| Model | Input Cost (per 1M tokens) | Output Cost (per 1M tokens) | Best For |
| ----------------- | -------------------------- | --------------------------- | ------------------------------ |
| Claude 3.5 Sonnet | $3.00 | $15.00 | Production apps, complex tasks |
| DeepSeek R1 | $1.00 | $3.00 | Budget-conscious production |
| DeepSeek V3 | $0.14 | $2.20 | Budget-conscious development |
#### Free Tier Benefits
**Vercel (Hobby)**
- 100 GB data transfer/month
- 100k serverless function invocations
- 100 MB deployment size
- Automatic HTTPS & CI/CD
**Supabase (Free)**
- 500 MB database storage
- 1 GB file storage
- 50k monthly active users
- 2M real-time messages/month
**GitHub (Free)**
- Unlimited public repositories
- GitHub Actions CI/CD
- Project management tools
- Collaboration features
### Getting Started
1. Install the development 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/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"
3. Add our recommended stack configuration:
- Create `.clinerules` file (see template below)
- Let Cline handle the rest!
#### Example Project Brief
```markdown
# Project Brief
## Overview
Building a [type of application] that will [main purpose].
## Core Features
- Feature 1
- Feature 2
- Feature 3
## Target Users
[Describe who will use your application]
## Technical Preferences (optional)
- Any specific technologies you want to use
- Any specific requirements or constraints
```
### .clinerules Template
```markdown
# Project Configuration
## Tech Stack
- Next.js 14+ with App Router
- Tailwind CSS for styling
- Supabase for backend
- Vercel for deployment
- GitHub for version control
## Project Structure
/src
/app # Next.js App Router pages
/components # React components
/lib # Utility functions
/types # TypeScript types
/supabase
/migrations # SQL migration files
/seed # Seed data files
/public # Static assets
## Database Migrations
SQL files in /supabase/migrations should:
- Use sequential numbering: 001, 002, etc.
- Include descriptive names
- Be reviewed by Cline before execution
Example: 001_create_users_table.sql
## Development Workflow
- Cline helps write and review code changes
- Vercel automatically deploys from main branch
- Database migrations reviewed by Cline before execution
## Security
DO NOT read or modify:
- .env files
- \*_/config/secrets._
- Any file containing API keys or credentials
```
### Learning Resources (2025)
Want to learn more about the technologies we're using? Here are some great resources:
#### Next.js and React
- [Official Learn Next.js Course](https://nextjs.org/learn) - Interactive tutorial
- [NextJS App Router: Modern Web Dev in 1 Hour](https://www.youtube.com/nextjs-modern) - Quick overview
- [Building Real-World Apps with Next.js](https://www.youtube.com/nextjs-real-world) - Practical examples
#### Supabase
- [Supabase From Scratch](https://www.udemy.com/supabase-scratch) - Comprehensive course
- [Official Quickstart Guides](https://supabase.com/docs/guides/getting-started)
- [Real-Time Apps with Next.js and Supabase](https://www.newline.co/courses/supabase-nextjs)
#### Tailwind CSS
- [Tailwind CSS Tutorial for Beginners](https://www.youtube.com/tailwind-2025)
- [Official Tailwind Documentation](https://tailwindcss.com/docs)
- Interactive course at [Scrimba Tailwind CSS Course](https://scrimba.com/learn/tailwind)
### Other Things to Know
#### Working with Git & GitHub
Git helps you track changes in your code and collaborate with others. Here are the essential commands you'll use:
**Daily Development**
```bash
# Save your changes (do this often!)
git add . # Stage all changed files
git commit -m "Add login page" # Save changes with a clear message
# Share your changes
git push origin main # Upload to GitHub
```
**Common Workflow**
1. **Start of day**: Get latest changes
```bash
git pull origin main # Download latest code
```
2. **During development**: Save work regularly
```bash
git add .
git commit -m "Clear message about changes"
```
3. **End of day**: Share your progress
```bash
git push origin main # Upload to GitHub
```
**Best Practices**
- Commit often with clear messages
- Pull before starting new work
- Push completed work to share with others
- Use `.gitignore` to avoid committing sensitive files
> **Tip**: Vercel automatically deploys when you push to main!
#### Environment Variables
- Store secrets in `.env.local` for development
- Add them to Vercel project settings for production
- Never commit `.env` files to Git
#### Getting Help
1. Use `/help` in Cline chat for immediate assistance
2. Check [Cline Documentation](https://docs.cline.bot)
3. Join our [Discord Community](https://discord.gg/cline)
4. Search GitHub issues for common problems
Remember: Cline is here to help at every step. Just ask for guidance or clarification when needed!
-10118
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -4,13 +4,15 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "mintlify dev"
"dev": "mintlify dev",
"check": "mintlify broken-links",
"rename": "mintlify rename"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"mintlify": "^4.0.538"
"mintlify": "^4.2.23"
}
}
+25
View File
@@ -32,6 +32,31 @@ First, you'll need to install and authenticate Claude Code on your system:
/>
</Frame>
<br />
<Accordion title="Windows Setup">
On Windows, Cline supports integrating with Claude Code through WSL.
Windows doesn't support long commands, and Claude Code only accepts the system prompt through a flag, which means we can't properly prompt Claude through Claude Code. Anthropic is [working on a workaround to streamline this](https://github.com/anthropics/claude-code/issues/3411).
1. **Make sure you have WSL set-up**. You can follow [this](https://code.visualstudio.com/docs/remote/wsl#_installation) guide to do it.
2. **Open VSCode from WSL** and verify it's properly set-up. You should see an indicator in the bottom left that says "WSL". You can find an image of the indicator [here](https://code.visualstudio.com/docs/remote/wsl#_from-the-wsl-terminal).
3. Install Cline within WSL and make sure it shows the following:
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline_wsl_installed_extension.webp"
alt="Indicator that an extension is installed on WSL"
/>
</Frame>
4. Clone or [move](https://stackoverflow.com/a/42586455) your project over to WSL
5. Follow the [instructions on how to set up Claude Code normally](#setup), but from the WSL terminal.
</Accordion>
### Finding your Claude Code path
If you're not sure where Claude Code is installed:
+39
View File
@@ -0,0 +1,39 @@
---
title: "SAP AI Core"
description: "Learn how to configure and use LLM models from Generative AI Hub in SAP AI Core with Cline."
---
SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new business processes in a cost-efficient manner.
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
### Getting a Service Binding
> 💡 **Information**
>
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance to perform these steps.
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
3. **Copy the Service Binding:** Copy the service binding values.
### Supported Models
SAP AI Core supports a large and growing number of models.
Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/3437766) for the complete and up-to-date list.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown.
3. **Enter Client Id:** Add the `.clientid` field from the service binding into the "AI Core Client Id" field.
4. **Enter Client Secret:** Add the `.clientsecret` field from the service binding into the "AI Core Client Secret" field.
5. **Enter Base URL:** Add the `.serviceurls.AI_API_URL` field from the service binding into the "AI Core Base URL" field.
6. **Enter Auth URL:** Add the `.url` field from the service binding into the "AI Core Auth URL" field.
7. **Enter Resource Group:** Add the resource group where you have your model deployments. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core).
8. **Select Model:** Choose your desired model from the "Model" dropdown.
### Tips and Notes
- **Model Selection:** SAP AI Core offers a wide range of models. You won't be able to use the model, even if selected, if a deployment doesn't exist in the provided resource group.
+12 -1
View File
@@ -5,6 +5,7 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
const destDir = standalone ? "dist-standalone" : "dist"
/**
@@ -160,8 +161,18 @@ const standaloneConfig = {
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
}
// E2E build script configuration
const e2eBuildConfig = {
...baseConfig,
entryPoints: ["src/test/e2e/utils/build.ts"],
outfile: `${destDir}/e2e-build.js`,
external: ["@vscode/test-electron", "execa"],
sourcemap: false,
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
}
async function main() {
const config = standalone ? standaloneConfig : extensionConfig
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
const extensionCtx = await esbuild.context(config)
if (watch) {
await extensionCtx.watch()
+61 -16
View File
@@ -11,8 +11,11 @@ const disallowedApis = {
"vscode.workspace.fs.stat": {
messageId: "useFsUtils",
},
"vscode.workspace.fs.writeFile": {
messageId: "useFsUtils",
},
"vscode.workspace.workspaceFolders": {
messageId: "useHostBridge",
messageId: "useHostBridgeWorkspace",
},
"vscode.workspace.asRelativePath": {
messageId: "usePathUtils",
@@ -20,6 +23,29 @@ const disallowedApis = {
"vscode.workspace.getWorkspaceFolder": {
messageId: "usePathUtils",
},
"vscode.window.showTextDocument": {
messageId: "useHostBridge",
},
"vscode.workspace.applyEdit": {
messageId: "useHostBridge",
},
// "vscode.env.openExternal": {
// messageId: "useUtils",
// },
// "vscode.window.showWarningMessage": {
// messageId: "useHostBridgeShowMessage",
// },
"vscode.window.showOpenDialog": {
messageId: "useHostBridgeShowMessage",
},
// There are too many warnings for these calls, uncomment the following
// when the migration is finished.
// "vscode.window.showErrorMessage": {
// messageId: "useHostBridgeShowMessage",
// },
// "vscode.window.showInformationMessage": {
// messageId: "useHostBridgeShowMessage",
// },
}
module.exports = createRule({
@@ -37,16 +63,28 @@ module.exports = createRule({
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
useFsUtils:
"Use utilities in @/utils/fs instead of vscode.workspace.fs.stat.\n" +
"Use utilities in @/utils/fs instead of vscode.workspace.fs\n" +
"Example: import { isDirectory } from '@/utils/fs' or use the file system methods from the host bridge provider.\n" +
"Found: {{code}}",
useHostBridge:
usePathUtils:
"Use path utilities from @/utils/path instead of VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
"Found: {{code}}",
useHostBridgeWorkspace:
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
usePathUtils:
"Use path utilities from @/utils/path instead of direct VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
useHostBridgeShowMessage:
"Use getHostBridgeProvider().windowClient.showMessage instead of the vscode.window.showMessage.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useHostBridge:
"Use the host bridge instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useUtils:
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
},
schema: [],
@@ -54,17 +92,10 @@ module.exports = createRule({
defaultOptions: [],
create(context) {
// Check if current file is in an exception directory or is grpc-client-base.ts
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
// Skip checking files in src/hosts/vscode or standalone/runtime-files
const isExceptionDirectory = filename.includes("/src/hosts/vscode/") || filename.includes("/standalone/runtime-files/")
// Pattern for checking memberExpressions like vscode.workspace.fs.stat
function checkMemberExpression(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isGrpcClientBase || isExceptionDirectory) {
if (isExcluded(context.filename)) {
// Skip if this file is being excluded.
return
}
@@ -143,6 +174,20 @@ module.exports = createRule({
})
}
function isExcluded(filename) {
// Check if current file is in an exception directory or is grpc-client-base.ts
if (path.basename(filename) === "grpc-client-base.ts") {
return true
}
// Skip checking files in src/hosts/vscode or standalone/runtime-files
if (filename.includes("/src/hosts/vscode/")) {
return true
}
if (filename.includes("/standalone/runtime-files/")) {
return true
}
}
return {
// Detect basic member expressions (e.g., vscode.postMessage)
MemberExpression(node) {
@@ -152,7 +197,7 @@ module.exports = createRule({
// Detect property access through destructuring
VariableDeclarator(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isGrpcClientBase || isExceptionDirectory) {
if (isExcluded(context.filename)) {
return
}
+1 -1
View File
@@ -925,7 +925,7 @@ async function main() {
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-25-25")
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--parallel", "Run tests in parallel", false)
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
+12 -2
View File
@@ -937,8 +937,18 @@ def main():
# Build current URL
# Dynamically derive the base URL
server_address = st.server.server_address if hasattr(st.server, 'server_address') else "localhost"
server_port = st.server.server_port if hasattr(st.server, 'server_port') else "8501"
try:
# For older Streamlit versions
server_address = st.server.server_address
server_port = st.server.server_port
except AttributeError:
# Fallback for newer Streamlit versions where st.server is removed
# We can't reliably get the server address/port from within the script anymore.
# We'll default to localhost and the default port.
# The user can see the correct network URL in the terminal.
server_address = "localhost"
server_port = 8501
base_url = f"http://{server_address}:{server_port}"
current_url = f"{base_url}/?run_id={st.session_state.selected_run_id}"
if st.session_state.drill_down_model:
+4728 -12487
View File
File diff suppressed because it is too large Load Diff
+10 -6
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.19.4",
"version": "3.19.7",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -345,6 +345,9 @@
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"test:coverage": "vscode-test --coverage",
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
@@ -354,9 +357,9 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs": "cd docs && mintlify dev",
"docs:check-links": "cd docs && mintlify broken-links",
"docs:rename-file": "cd docs && mintlify rename",
"docs": "cd docs && npm run dev",
"docs:check-links": "cd docs && npm run check",
"docs:rename-file": "cd docs && npm run rename",
"report-issue": "node scripts/report-issue.js"
},
"lint-staged": {
@@ -383,7 +386,8 @@
"@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/utils": "^8.33.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
@@ -393,7 +397,6 @@
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"mintlify": "^4.0.515",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
@@ -425,6 +428,7 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@playwright/test": "^1.53.2",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@vscode/codicons": "^0.0.36",
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from "@playwright/test"
const isGitHubAction = !!process.env.CI
export default defineConfig({
workers: 1,
retries: 1,
testDir: "src/test/e2e",
timeout: 20000,
expect: {
timeout: 20000,
},
fullyParallel: true,
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
})
+40 -2
View File
@@ -10,7 +10,16 @@ import "common.proto";
service DiffService {
// Open the diff view/editor.
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
// Get the contents of the diff view.
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
// Replace a text selection in the diff.
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
// Truncate the diff document.
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
// Save the diff document.
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
// Close the diff editor UI.
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
}
message OpenDiffRequest {
@@ -26,6 +35,15 @@ message OpenDiffResponse {
optional string diff_id = 1;
}
message GetDocumentTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message GetDocumentTextResponse {
optional string content = 1;
}
message ReplaceTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
@@ -34,6 +52,26 @@ message ReplaceTextRequest {
optional int32 end_line = 5;
}
message ReplaceTextResponse {
// TBD
message ReplaceTextResponse {}
message TruncateDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional int32 end_line = 5;
}
message TruncateDocumentResponse {}
message CloseDiffRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message CloseDiffResponse {}
message SaveDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message SaveDocumentResponse {}
+4
View File
@@ -6,6 +6,10 @@ option java_multiple_files = true;
import "common.proto";
/**
* The watch service is only here as example of a streaming rpc in the host bridge.
* This being replaced with a native JS file watcher.
*/
// WatchService provides methods for watching files in the IDE
service WatchService {
// Subscribe to file changes
+124 -81
View File
@@ -15,6 +15,8 @@ service ModelsService {
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hugging Face models
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
@@ -23,6 +25,8 @@ service ModelsService {
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Refreshes and returns Groq models
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -120,9 +124,11 @@ enum ApiProvider {
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
MOONSHOT = 26;
GROQ = 24;
SAPAICORE = 25;
CLAUDE_CODE = 26;
MOONSHOT = 27;
HUGGINGFACE = 28;
}
// Model info for OpenAI-compatible models
@@ -162,82 +168,119 @@ message LiteLLMModelInfo {
// Main ApiConfiguration message
message ModelsApiConfiguration {
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_account_id = 3;
optional string task_id = 4;
optional string lite_llm_base_url = 5;
optional string lite_llm_model_id = 6;
optional string lite_llm_api_key = 7;
optional bool lite_llm_use_prompt_cache = 8;
map<string, string> open_ai_headers = 9;
optional LiteLLMModelInfo lite_llm_model_info = 10;
optional string anthropic_base_url = 11;
optional string open_router_api_key = 12;
optional string open_router_model_id = 13;
optional OpenRouterModelInfo open_router_model_info = 14;
optional string open_router_provider_sorting = 15;
optional string aws_access_key = 16;
optional string aws_secret_key = 17;
optional string aws_session_token = 18;
optional string aws_region = 19;
optional bool aws_use_cross_region_inference = 20;
optional bool aws_bedrock_use_prompt_cache = 21;
optional bool aws_use_profile = 22;
optional string aws_profile = 23;
optional string aws_bedrock_endpoint = 24;
optional bool aws_bedrock_custom_selected = 25;
optional string aws_bedrock_custom_model_base_id = 26;
optional string vertex_project_id = 27;
optional string vertex_region = 28;
optional string open_ai_base_url = 29;
optional string open_ai_api_key = 30;
optional string open_ai_model_id = 31;
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
optional string ollama_model_id = 33;
optional string ollama_base_url = 34;
optional string ollama_api_options_ctx_num = 35;
optional string lm_studio_model_id = 36;
optional string lm_studio_base_url = 37;
optional string gemini_api_key = 38;
optional string gemini_base_url = 39;
optional string open_ai_native_api_key = 40;
optional string deep_seek_api_key = 41;
optional string requesty_api_key = 42;
optional string requesty_model_id = 43;
optional OpenRouterModelInfo requesty_model_info = 44;
optional string together_api_key = 45;
optional string together_model_id = 46;
optional string fireworks_api_key = 47;
optional string fireworks_model_id = 48;
optional int32 fireworks_model_max_completion_tokens = 49;
optional int32 fireworks_model_max_tokens = 50;
optional string qwen_api_key = 51;
optional string doubao_api_key = 52;
optional string mistral_api_key = 53;
optional string azure_api_version = 54;
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
optional string qwen_api_line = 56;
optional string nebius_api_key = 57;
optional string asksage_api_url = 58;
optional string asksage_api_key = 59;
optional string xai_api_key = 60;
optional int32 thinking_budget_tokens = 61;
optional string reasoning_effort = 62;
optional string sambanova_api_key = 63;
optional string cerebras_api_key = 64;
optional int32 request_timeout_ms = 65;
optional ApiProvider api_provider = 66;
repeated string favorited_model_ids = 67;
optional string sap_ai_core_client_id = 68;
optional string sap_ai_core_client_secret = 69;
optional string sap_ai_resource_group = 70;
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
optional string aws_authentication = 74;
optional string aws_bedrock_api_key = 75;
optional string moonshot_api_key = 76;
optional string moonshot_api_line = 77;
// Global configuration fields (not mode-specific)
optional string api_key = 1;
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
map<string, string> open_ai_headers = 7;
optional string anthropic_base_url = 8;
optional string open_router_api_key = 9;
optional string open_router_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string open_ai_base_url = 23;
optional string open_ai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string open_ai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int32 fireworks_model_max_completion_tokens = 35;
optional int32 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int32 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
optional string aws_authentication = 56;
optional string aws_bedrock_api_key = 57;
optional string cline_account_id = 58;
optional string groq_api_key = 59;
optional string hugging_face_api_key = 60;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int32 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_requesty_model_id = 115;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_groq_model_id = 120;
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
optional string plan_mode_hugging_face_model_id = 122;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int32 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_requesty_model_id = 215;
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_groq_model_id = 220;
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
optional string act_mode_hugging_face_model_id = 222;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
repeated string favorited_model_ids = 300;
}
+102 -115
View File
@@ -118,126 +118,113 @@ message UpdateSettingsRequest {
// Complete API Configuration message
message ApiConfiguration {
// Core API fields
optional string api_provider = 1;
optional string api_model_id = 2;
optional string api_key = 3; // anthropic
optional string api_base_url = 4;
// Global configuration fields (not mode-specific)
optional string api_key = 1; // anthropic
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
optional string openai_headers = 7; // JSON string
optional string anthropic_base_url = 8;
optional string openrouter_api_key = 9;
optional string openrouter_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string openai_base_url = 23;
optional string openai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string openai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int64 fireworks_model_max_completion_tokens = 35;
optional int64 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int64 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
// Provider-specific API keys
optional string cline_account_id = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
optional string openai_native_api_key = 9;
optional string gemini_api_key = 10;
optional string deepseek_api_key = 11;
optional string requesty_api_key = 12;
optional string together_api_key = 13;
optional string fireworks_api_key = 14;
optional string qwen_api_key = 15;
optional string doubao_api_key = 16;
optional string mistral_api_key = 17;
optional string nebius_api_key = 18;
optional string asksage_api_key = 19;
optional string xai_api_key = 20;
optional string sambanova_api_key = 21;
optional string cerebras_api_key = 22;
// Plan mode configurations
optional string plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int64 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_openrouter_model_id = 107;
optional string plan_mode_openrouter_model_info = 108; // JSON string
optional string plan_mode_openai_model_id = 109;
optional string plan_mode_openai_model_info = 110; // JSON string
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional string plan_mode_lite_llm_model_info = 114; // JSON string
optional string plan_mode_requesty_model_id = 115;
optional string plan_mode_requesty_model_info = 116; // JSON string
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
// Model IDs
optional string openrouter_model_id = 23;
optional string openai_model_id = 24;
optional string anthropic_model_id = 25;
optional string bedrock_model_id = 26;
optional string vertex_model_id = 27;
optional string gemini_model_id = 28;
optional string ollama_model_id = 29;
optional string lm_studio_model_id = 30;
optional string litellm_model_id = 31;
optional string requesty_model_id = 32;
optional string together_model_id = 33;
optional string fireworks_model_id = 34;
// AWS Bedrock fields
optional bool aws_bedrock_custom_selected = 35;
optional string aws_bedrock_custom_model_base_id = 36;
optional string aws_access_key = 37;
optional string aws_secret_key = 38;
optional string aws_session_token = 39;
optional string aws_region = 40;
optional bool aws_use_cross_region_inference = 41;
optional bool aws_bedrock_use_prompt_cache = 42;
optional bool aws_use_profile = 43;
optional string aws_profile = 44;
optional string aws_bedrock_endpoint = 45;
// Vertex AI fields
optional string vertex_project_id = 46;
optional string vertex_region = 47;
// Base URLs and endpoints
optional string openai_base_url = 48;
optional string ollama_base_url = 49;
optional string lm_studio_base_url = 50;
optional string gemini_base_url = 51;
optional string litellm_base_url = 52;
optional string asksage_api_url = 53;
// LiteLLM specific fields
optional string litellm_api_key = 54;
optional bool litellm_use_prompt_cache = 55;
// Model configuration
optional int64 thinking_budget_tokens = 56;
optional string reasoning_effort = 57;
optional int64 request_timeout_ms = 58;
// Fireworks specific
optional int64 fireworks_model_max_completion_tokens = 59;
optional int64 fireworks_model_max_tokens = 60;
// Azure specific
optional string azure_api_version = 61;
// Ollama specific
optional string ollama_api_options_ctx_num = 62;
// Qwen specific
optional string qwen_api_line = 63;
// OpenRouter specific
optional string openrouter_provider_sorting = 64;
// VSCode LM (stored as JSON string due to complex type)
optional string vscode_lm_model_selector = 65;
// Model info objects (stored as JSON strings)
optional string openrouter_model_info = 66;
optional string openai_model_info = 67;
optional string requesty_model_info = 68;
optional string litellm_model_info = 69;
// OpenAI headers (stored as JSON string)
optional string openai_headers = 70;
// Act mode configurations
optional string act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int64 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_openrouter_model_id = 207;
optional string act_mode_openrouter_model_info = 208; // JSON string
optional string act_mode_openai_model_id = 209;
optional string act_mode_openai_model_info = 210; // JSON string
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional string act_mode_lite_llm_model_info = 214; // JSON string
optional string act_mode_requesty_model_id = 215;
optional string act_mode_requesty_model_info = 216; // JSON string
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
// Favorited model IDs
repeated string favorited_model_ids = 71;
// SAP AI Core specific
optional string sap_ai_core_client_id = 72;
optional string sap_ai_core_client_secret = 73;
optional string sap_ai_core_base_url = 74;
optional string sap_ai_core_token_url = 75;
optional string sap_ai_resource_group = 76;
// Claude Code specific
optional string claude_code_path = 77;
repeated string favorited_model_ids = 300;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
optional string aws_authentication = 301;
optional string aws_bedrock_api_key = 302;
// Moonshot
optional string moonshot_api_key = 80;
optional string moonshot_api_line = 81;
optional string cline_account_id = 303;
}
+13 -176
View File
@@ -9,7 +9,7 @@ import chalk from "chalk"
import os from "os"
import { createRequire } from "module"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.mjs"
import { serviceNameMap } from "./build-proto-config.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
@@ -42,6 +42,8 @@ const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("s
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
await cleanup()
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
@@ -50,8 +52,6 @@ async function main() {
await fs.mkdir(dir, { recursive: true })
}
await cleanup()
// Check for missing proto files for services in serviceNameMap
await ensureProtoFilesExist()
@@ -88,9 +88,6 @@ async function main() {
await generateProtoBusMethodRegistrations()
await generateProtoBusGrpcClientConfig()
await generateHostBridgeServiceConfig()
await generateHostBridgeMethodRegistrations()
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
}
@@ -420,180 +417,16 @@ service ${serviceClassName} {
}
}
/**
* Generate method registration files for host services
*/
async function generateHostBridgeMethodRegistrations() {
log_verbose(chalk.cyan("Generating host method registration files..."))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join("src/hosts/vscode/hostbridge", serviceKey),
)
// Parse proto files for streaming methods
const hostProtoFiles = await globby("*.proto", { cwd: path.join(PROTO_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(PROTO_DIR, "host"))
for (const serviceDir of hostServiceDirs) {
const serviceName = path.basename(serviceDir)
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
const outputDir = path.join("src/generated/hosts/vscode/hostbridge", serviceName)
log_verbose(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 ${SCRIPT_NAME}
// 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 "@hosts/vscode/hostbridge/${serviceName}/${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
const registryFile = path.join(outputDir, "methods.ts")
await writeFileWithMkdirs(registryFile, methodsContent)
log_verbose(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 ${SCRIPT_NAME}
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "@hosts/vscode/hostbridge-grpc-service"
import { StreamingResponseHandler } from "@hosts/vscode/hostbridge-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
const indexFile = path.join(outputDir, "index.ts")
await writeFileWithMkdirs(indexFile, indexContent)
log_verbose(chalk.green(`Generated ${indexFile}`))
}
log_verbose(chalk.green("Host method registration files generated successfully."))
}
/**
* Generate a service configuration file for host services
*/
async function generateHostBridgeServiceConfig() {
log_verbose(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 "@generated/hosts/vscode/hostbridge/${dirName}/index"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
requestHandler: handle${capitalizedName}ServiceRequest,
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
}`)
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by ${SCRIPT_NAME}
import { StreamingResponseHandler } from "@/hosts/vscode/hostbridge-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 filePath = "src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
}
async function cleanup() {
// Clean up existing generated files
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir("src/generated")
await rmrf(TS_OUT_DIR)
await rmrf("src/generated")
// Clean up generated files that were moved.
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
await rmdir("src/standalone/services")
await fs.rm("hosts/vscode", { force: true, recursive: true })
await rmdir("hosts")
await fs.rm("src/standalone/server-setup.ts", { force: true })
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
await rmrf("src/standalone/services/host-grpc-client.ts")
await rmrf("src/standalone/server-setup.ts")
await rmrf("src/hosts/vscode/host-grpc-service-config.ts")
const oldhostbridgefiles = [
"src/hosts/vscode/workspace/methods.ts",
"src/hosts/vscode/workspace/index.ts",
@@ -609,7 +442,7 @@ async function cleanup() {
"src/hosts/vscode/uri/index.ts",
]
for (const file of oldhostbridgefiles) {
await fs.rm(file, { force: true })
await rmrf(file)
}
}
@@ -635,6 +468,10 @@ async function rmdir(path) {
}
}
async function rmrf(path) {
await fs.rm(path, { force: true, recursive: true })
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+89 -21
View File
@@ -6,10 +6,15 @@ import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import chalk from "chalk"
const IMPL_FILE = path.resolve("src/generated/standalone/host-bridge-clients.ts")
const INTERFACE_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
// Contains the interface definitions for the host bridge clients.
const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
// Contains the ExternalHostBridgeClientManager for the external host bridge clients (using nice-grpc).
const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-bridge-clients.ts")
// Contains the handler map for the external host bridge clients (using the custom service registry).
const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
const typeNameToFQN = new Map()
function addTypeNameToFqn(name, fqn) {
@@ -48,24 +53,23 @@ async function main() {
}
}
// Generate interfaces file
await generateInterfacesFile(hostServices)
// // Generate implementation file
await generateImplementationFile(hostServices)
await generateTypesFile(hostServices)
await generateExternalClientFile(hostServices)
await generateVscodeClientFile(hostServices)
console.log(`Generated host bridge client files at:`)
console.log(`- ${INTERFACE_FILE}`)
console.log(`- ${IMPL_FILE}`)
console.log(`- ${TYPES_FILE}`)
console.log(`- ${EXTERNAL_CLIENT_FILE}`)
console.log(`- ${VSCODE_CLIENT_FILE}`)
}
/**
* Generate the client interfaces file.
*/
async function generateInterfacesFile(hostServices) {
async function generateTypesFile(hostServices) {
const clientInterfaces = []
for (const [name, def] of Object.entries(hostServices)) {
const clientInterface = generateClientInterface(name, def)
const clientInterface = generateClientInterfaceType(name, def)
clientInterfaces.push(clientInterface)
}
const content = `// GENERATED CODE -- DO NOT EDIT!
@@ -76,14 +80,14 @@ import { StreamingCallbacks } from "@hosts/host-provider-types"
${clientInterfaces.join("\n\n")}
`
// Write output file
await fs.mkdir(path.dirname(INTERFACE_FILE), { recursive: true })
await fs.writeFile(INTERFACE_FILE, content)
await fs.mkdir(path.dirname(TYPES_FILE), { recursive: true })
await fs.writeFile(TYPES_FILE, content)
}
/**
* Generate a client interface for a service.
*/
function generateClientInterface(serviceName, serviceDefinition) {
function generateClientInterfaceType(serviceName, serviceDefinition) {
// Get the methods from the service definition
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
@@ -110,9 +114,9 @@ ${methods}
}
/**
* Generate the client implementations file.
* Generate the external client implementations file.
*/
async function generateImplementationFile(hostServices) {
async function generateExternalClientFile(hostServices) {
// Generate imports
const imports = []
// Add imports for the interfaces
@@ -121,7 +125,7 @@ async function generateImplementationFile(hostServices) {
}
const clientImplementations = []
for (const [name, def] of Object.entries(hostServices)) {
clientImplementations.push(generateClientImplementation(name, def))
clientImplementations.push(generateExternalClientSetup(name, def))
}
const content = `// GENERATED CODE -- DO NOT EDIT!
@@ -136,16 +140,15 @@ ${imports.join("\n")}
${clientImplementations.join("\n\n")}
`
// Write output file
await fs.mkdir(path.dirname(IMPL_FILE), { recursive: true })
await fs.writeFile(IMPL_FILE, content)
await fs.mkdir(path.dirname(EXTERNAL_CLIENT_FILE), { recursive: true })
await fs.writeFile(EXTERNAL_CLIENT_FILE, content)
}
/**
* Generate a client implementation class for a service
*/
function generateClientImplementation(serviceName, serviceDefinition) {
function generateExternalClientSetup(serviceName, serviceDefinition) {
// Get the methods from the service definition
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
@@ -185,6 +188,71 @@ ${methods}
}`
}
/**
* Generate the Vscode client setup file.
*/
async function generateVscodeClientFile(hostServices) {
const imports = []
const clientImplementations = []
const handlerMap = []
for (const [serviceName, serviceDefinition] of Object.entries(hostServices)) {
const name = serviceName.replace(/Service$/, "").toLowerCase()
for (const [methodName, _methodDef] of Object.entries(serviceDefinition.service)) {
imports.push(`import { ${methodName} } from "@/hosts/vscode/hostbridge/${name}/${methodName}"`)
}
imports.push("")
clientImplementations.push(generateVscodeClientImplementation(name, serviceDefinition))
handlerMap.push(` "host.${serviceName}": {
requestHandler: ${name}ServiceRegistry.handleRequest,
streamingHandler: ${name}ServiceRegistry.handleStreamingRequest,
},`)
}
const content = `// GENERATED CODE -- DO NOT EDIT!
// Generated by scripts/generate-host-bridge-client.mjs
import { createServiceRegistry } from "@hosts/vscode/hostbridge-grpc-service"
import { HostServiceHandlerConfig } from "@hosts/vscode/hostbridge-grpc-handler"
${imports.join("\n")}
${clientImplementations.join("\n\n")}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
${handlerMap.join("\n")}
}
`
// Write output file
await fs.mkdir(path.dirname(VSCODE_CLIENT_FILE), { recursive: true })
await fs.writeFile(VSCODE_CLIENT_FILE, content)
}
function generateVscodeClientImplementation(serviceName, serviceDefinition) {
// Get the methods from the service definition
const name = serviceName.replace(/Service$/, "").toLowerCase()
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
// Get fully qualified type names
const isStreamingResponse = methodDef.responseStream
if (!isStreamingResponse) {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
} else {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
}
})
.join("\n")
// Generate the class
return `// Setup ${name} service registry
const ${name}ServiceRegistry = createServiceRegistry("${name}")
${methods}`
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
+1 -1
View File
@@ -62,7 +62,7 @@ let output = `// GENERATED CODE -- DO NOT EDIT!
import * as grpc from "@grpc/grpc-js"
import { cline } from "@generated/grpc-js"
import { Controller } from "@core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
${imports}
export function addProtobusServices(
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eu #x
# This compiles the cline-core app, installs it to the user's home directory,
# and runs the service.
CORE_DIR=~/.cline/core
INSTALL_DIR=$CORE_DIR/0.0.1
# Build cline core
npm run compile-standalone
# Remove old unpacked versions to force reinstall
rm -rf $CORE_DIR/* || true
mkdir -p $INSTALL_DIR
cp dist-standalone/standalone.zip $INSTALL_DIR
cd $INSTALL_DIR
unp standalone.zip > /dev/null
pkill -f cline-core.js || true
NODE_PATH=./node_modules node cline-core.js
+103 -60
View File
@@ -28,6 +28,9 @@ import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
import { GroqHandler } from "./providers/groq"
import { Mode } from "../shared/ChatSettings"
import { HuggingFaceHandler } from "./providers/huggingface"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -39,27 +42,33 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
function createHandlerForProvider(
apiProvider: string | undefined,
options: Omit<ApiConfiguration, "apiProvider">,
mode: Mode,
): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler({
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
@@ -71,16 +80,20 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
awsBedrockCustomSelected:
mode === "plan" ? options.planModeAwsBedrockCustomSelected : options.actModeAwsBedrockCustomSelected,
awsBedrockCustomModelBaseId:
mode === "plan" ? options.planModeAwsBedrockCustomModelBaseId : options.actModeAwsBedrockCustomModelBaseId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "vertex":
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
@@ -91,21 +104,21 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: options.openAiModelId,
openAiModelInfo: options.openAiModelInfo,
reasoningEffort: options.reasoningEffort,
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
})
case "ollama":
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: options.ollamaModelId,
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: options.lmStudioModelId,
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
})
case "gemini":
return new GeminiHandler({
@@ -113,78 +126,85 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "deepseek":
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "requesty":
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
requestyModelId: options.requestyModelId,
requestyModelInfo: options.requestyModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
requestyModelId: mode === "plan" ? options.planModeRequestyModelId : options.actModeRequestyModelId,
requestyModelInfo: mode === "plan" ? options.planModeRequestyModelInfo : options.actModeRequestyModelInfo,
})
case "fireworks":
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: options.fireworksModelId,
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: options.togetherModelId,
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
})
case "qwen":
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "mistral":
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler({
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
vsCodeLmModelSelector:
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
})
case "cline":
return new ClineHandler({
clineAccountId: options.clineAccountId,
taskId: options.taskId,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: options.liteLlmModelId,
liteLlmModelInfo: options.liteLlmModelInfo,
thinkingBudgetTokens: options.thinkingBudgetTokens,
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
liteLlmModelInfo: mode === "plan" ? options.planModeLiteLlmModelInfo : options.actModeLiteLlmModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
@@ -192,34 +212,48 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
huggingFaceApiKey: options.huggingFaceApiKey,
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
huggingFaceModelInfo:
mode === "plan" ? options.planModeHuggingFaceModelInfo : options.actModeHuggingFaceModelInfo,
})
case "nebius":
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "asksage":
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "xai":
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sambanova":
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "cerebras":
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "groq":
return new GroqHandler({
groqApiKey: options.groqApiKey,
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sapaicore":
return new SapAiCoreHandler({
@@ -228,37 +262,46 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "claude-code":
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
default:
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
}
}
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): ApiHandler {
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
// Validate thinking budget tokens against model's maxTokens to prevent API errors
// wrapped in a try-catch for safety, but this should never throw
try {
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options)
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options, mode)
const modelInfo = handler.getModel().info
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
if (modelInfo.maxTokens && thinkingBudgetTokens > modelInfo.maxTokens) {
const clippedValue = modelInfo.maxTokens - 1
options.thinkingBudgetTokens = clippedValue
if (mode === "plan") {
options.planModeThinkingBudgetTokens = clippedValue
} else {
options.actModeThinkingBudgetTokens = clippedValue
}
} else {
return handler // don't rebuild unless its necessary
}
@@ -267,5 +310,5 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
console.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options)
return createHandlerForProvider(apiProvider, options, mode)
}
+13 -13
View File
@@ -203,7 +203,7 @@ describe("AwsBedrockHandler", () => {
})
const mockOptions: ApiHandlerOptions = {
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
awsRegion: "us-east-1",
awsAccessKey: "test-key",
awsSecretKey: "test-secret",
@@ -214,9 +214,9 @@ describe("AwsBedrockHandler", () => {
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
thinkingBudgetTokens: 1600,
actModeAwsBedrockCustomSelected: false,
actModeAwsBedrockCustomModelBaseId: undefined,
actModeThinkingBudgetTokens: 1600,
}
const mockModelInfo = {
@@ -616,8 +616,8 @@ describe("AwsBedrockHandler", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -631,8 +631,8 @@ describe("AwsBedrockHandler", () => {
it("should not encode custom model IDs with slashes", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "my-namespace/my-custom-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "my-namespace/my-custom-model",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -680,8 +680,8 @@ describe("AwsBedrockHandler", () => {
it("should not apply cross-region prefix for custom models even when enabled", async () => {
const customCrossRegionOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsUseCrossRegionInference: true,
}
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
@@ -693,10 +693,10 @@ describe("AwsBedrockHandler", () => {
it("should handle UltraThink model ARN correctly", async () => {
const ultraThinkOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
+1 -1
View File
@@ -26,7 +26,7 @@ describe("OllamaHandler", () => {
beforeEach(() => {
options = {
ollamaModelId: "llama2",
actModeOllamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new OllamaHandler(options)
+1 -1
View File
@@ -13,7 +13,7 @@ interface AnthropicHandlerOptions {
}
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AnthropicHandlerOptions
private client: Anthropic | undefined
constructor(options: AnthropicHandlerOptions) {
+3 -16
View File
@@ -12,6 +12,7 @@ import OpenAI from "openai"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { clineEnvConfig } from "@/config"
interface ClineHandlerOptions {
taskId?: string
@@ -28,10 +29,7 @@ export class ClineHandler implements ApiHandler {
private clineAccountService = ClineAccountService.getInstance()
private _authService: AuthService
private client: OpenAI | undefined
// TODO: replace this with a global API Host
private readonly _baseUrl = "https://api.cline.bot"
// private readonly _baseUrl = "https://core-api.staging.int.cline.bot"
// private readonly _baseUrl = "http://localhost:7777"
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
lastGenerationId?: string
private counter = 0
@@ -179,18 +177,7 @@ export class ClineHandler implements ApiHandler {
}
} catch (error) {
console.error("Cline API Error:", error)
const requestId = error?.request_id ? ` (Request ID: ${error.request_id})` : ""
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
} else if (error.code === "insufficient_credits" || error.status === 402) {
if (error.error) {
error.error.message = error.error.message + requestId
throw new Error(JSON.stringify(error.error))
}
}
const _error = error instanceof Error ? error : new Error(String(error))
_error.message = _error.message + requestId
throw _error
throw error
}
}
+13 -18
View File
@@ -44,7 +44,7 @@ interface GeminiHandlerOptions {
* 4. Separating immediate costs from ongoing costs to avoid double-counting
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: GeminiHandlerOptions
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
@@ -256,23 +256,18 @@ export class GeminiHandler implements ApiHandler {
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
if (this.options.taskId) {
telemetryService.captureGeminiApiPerformance(
this.options.taskId,
modelId,
{
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
},
true,
)
telemetryService.captureGeminiApiPerformance(this.options.taskId, modelId, {
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
})
} else {
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
}
+290
View File
@@ -0,0 +1,290 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface GroqHandlerOptions {
groqApiKey?: string
groqModelId?: string
groqModelInfo?: ModelInfo
apiModelId?: string // For backward compatibility
}
// Model family definitions for enhanced behavior
interface GroqModelFamily {
name: string
supportedFeatures: {
streaming: boolean
temperature: boolean
vision: boolean
tools: boolean
}
maxTokensOverride?: number
specialParams?: Record<string, any>
}
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
// Moonshort 4 Family - Latest generation with vision support
"kimi-k2": {
name: "kimi-k2",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 4 Family - Latest generation with vision support
llama4: {
name: "Llama 4",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 3.3 Family - Balanced performance
"llama3.3": {
name: "Llama 3.3",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Llama 3.1 Family - Fast inference
"llama3.1": {
name: "Llama 3.1",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 131072,
},
// DeepSeek Family - Reasoning-optimized
deepseek: {
name: "DeepSeek",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
specialParams: {
top_p: 0.95,
reasoning_format: "parsed",
},
},
// Qwen Family - Enhanced for Q&A
qwen: {
name: "Qwen",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Compound Models - Hybrid architectures
compound: {
name: "Compound",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
},
}
export class GroqHandler implements ApiHandler {
private options: GroqHandlerOptions
private client: OpenAI | undefined
constructor(options: GroqHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.groqApiKey) {
throw new Error("Groq API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: this.options.groqApiKey,
})
} catch (error) {
throw new Error(`Error creating Groq client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
yield {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost,
}
}
/**
* Detects the model family based on the model ID
*/
private detectModelFamily(modelId: string): GroqModelFamily {
if (modelId.includes("kimi-k2")) {
return MODEL_FAMILIES["kimi-k2"]
}
// Llama 4 variants
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
return MODEL_FAMILIES.llama4
}
// Llama 3.3 variants
if (modelId.includes("llama-3.3")) {
return MODEL_FAMILIES["llama3.3"]
}
// Llama 3.1 variants
if (modelId.includes("llama-3.1")) {
return MODEL_FAMILIES["llama3.1"]
}
// DeepSeek variants
if (modelId.includes("deepseek")) {
return MODEL_FAMILIES.deepseek
}
// Qwen variants
if (modelId.includes("qwen")) {
return MODEL_FAMILIES.qwen
}
// Compound variants
if (modelId.includes("compound")) {
return MODEL_FAMILIES.compound
}
// Default fallback to Llama 3.3 behavior
return MODEL_FAMILIES["kimi-k2"]
}
/**
* Gets the optimal max_tokens based on model family and capabilities
*/
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
// Use model-specific max tokens if available
if (model.info.maxTokens && model.info.maxTokens > 0) {
return model.info.maxTokens
}
// Use family override if available
if (modelFamily.maxTokensOverride) {
return modelFamily.maxTokensOverride
}
// Default fallback
return 8192
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
// Optimize parameters based on model family
const temperature = 0
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// Build request parameters with model-specific optimizations
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
reasoning_format?: "parsed" | "raw" | "hidden"
top_p?: number
} = {
model: model.id,
max_tokens: maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature,
}
// Add any special parameters for specific model families
if (modelFamily.specialParams) {
Object.assign(requestParams, modelFamily.specialParams)
}
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Handle reasoning field if present (for reasoning models with parsed output)
if ((delta as any)?.reasoning) {
const reasoningContent = (delta as any).reasoning as string
yield {
type: "reasoning",
reasoning: reasoningContent,
}
continue
}
// Handle content field - trust the parsed output from Groq
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Handle usage information
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
}
/**
* Checks if the current model supports vision/images
*/
supportsImages(): boolean {
const model = this.getModel()
return model.info.supportsImages === true
}
/**
* Checks if the current model supports tools
*/
supportsTools(): boolean {
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
return modelFamily.supportedFeatures.tools
}
/**
* Gets model information with enhanced family detection
*/
getModel(): { id: string; info: ModelInfo } {
// First priority: groqModelId and groqModelInfo (like Requesty does)
const groqModelId = this.options.groqModelId
const groqModelInfo = this.options.groqModelInfo
if (groqModelId && groqModelInfo) {
return { id: groqModelId, info: groqModelInfo }
}
// Second priority: groqModelId with static model info
if (groqModelId && groqModelId in groqModels) {
const id = groqModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Third priority: apiModelId (for backward compatibility)
const apiModelId = this.options.apiModelId
if (apiModelId && apiModelId in groqModels) {
const id = apiModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Default fallback
return {
id: groqDefaultModelId,
info: groqModels[groqDefaultModelId],
}
}
/**
* Gets model family information for debugging/introspection
*/
getModelFamily(): GroqModelFamily {
const model = this.getModel()
return this.detectModelFamily(model.id)
}
}
+142
View File
@@ -0,0 +1,142 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface HuggingFaceHandlerOptions {
huggingFaceApiKey?: string
huggingFaceModelId?: string
huggingFaceModelInfo?: ModelInfo
}
export class HuggingFaceHandler implements ApiHandler {
private options: HuggingFaceHandlerOptions
private client: OpenAI | undefined
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
constructor(options: HuggingFaceHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.huggingFaceApiKey) {
throw new Error("Hugging Face API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.huggingface.co/v1",
apiKey: this.options.huggingFaceApiKey,
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
if (!usage) {
return
}
const inputTokens = usage.prompt_tokens || 0
const outputTokens = usage.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
const usageData = {
type: "usage" as const,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: totalCost,
}
yield usageData
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const stream = (await client.chat.completions.create(requestParams)) as any
let chunkCount = 0
let totalContent = ""
for await (const chunk of stream) {
chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
totalContent += delta.content
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
} catch (error: any) {
throw error
}
}
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
// Return cached model if available
if (this.cachedModel) {
return this.cachedModel
}
const modelId = this.options.huggingFaceModelId
// List all available models for debugging
const availableModels = Object.keys(huggingFaceModels)
let result: { id: HuggingFaceModelId; info: ModelInfo }
if (modelId && modelId in huggingFaceModels) {
const id = modelId as HuggingFaceModelId
const modelInfo = huggingFaceModels[id]
result = { id, info: modelInfo }
} else {
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
result = {
id: huggingFaceDefaultModelId,
info: defaultInfo,
}
}
// Cache the result for future calls
this.cachedModel = result
return result
}
}
+9
View File
@@ -19,6 +19,15 @@ interface OpenRouterHandlerOptions {
thinkingBudgetTokens?: number
}
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
export class OpenRouterHandler implements ApiHandler {
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
+6
View File
@@ -14,6 +14,12 @@ interface XAIHandlerOptions {
apiModelId?: string
}
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class XAIHandler implements ApiHandler {
private options: XAIHandlerOptions
private client: OpenAI | undefined
+4 -2
View File
@@ -140,7 +140,7 @@ export async function createOpenRouterStream(
}
// hardcoded provider sorting for kimi-k2
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
const isKimiK2 = model.id === "moonshotai/kimi-k2"
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
// @ts-ignore-next-line
@@ -158,7 +158,9 @@ export async function createOpenRouterStream(
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
// limit providers to only those that support the 131k context window
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
...(isKimiK2
? { provider: { order: ["groq", "together", "baseten", "parasail", "novita", "deepinfra"], allow_fallbacks: false } }
: {}),
})
return stream
+58
View File
@@ -0,0 +1,58 @@
export type Environment = "production" | "staging" | "local"
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
interface EnvironmentConfig {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
firebase: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId?: string
}
}
const configs: Record<Environment, EnvironmentConfig> = {
production: {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
authDomain: "cline-prod.firebaseapp.com",
projectId: "cline-prod",
storageBucket: "cline-prod.firebasestorage.app",
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
},
staging: {
appBaseUrl: "https://staging-app.cline.bot",
apiBaseUrl: "https://core-api.staging.int.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
authDomain: "cline-staging.firebaseapp.com",
projectId: "cline-staging",
storageBucket: "cline-staging.firebasestorage.app",
messagingSenderId: "853479478430",
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
},
},
local: {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
authDomain: "cline-preview.firebaseapp.com",
projectId: "cline-preview",
},
},
}
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
+8 -12
View File
@@ -44,12 +44,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
if (fileExists) {
const message = `${fileTypeName} file "${request.filename}" already exists.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
})
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
@@ -63,12 +61,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
await handleFileServiceRequest(controller, "openFile", { value: filePath })
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
}
return RuleFile.create({
+4 -6
View File
@@ -46,12 +46,10 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
const message = `${fileTypeName} file "${fileName}" deleted successfully`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
return RuleFile.create({
filePath: request.rulePath,
+103 -241
View File
@@ -10,7 +10,7 @@ import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
@@ -36,6 +36,7 @@ import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceC
import { AuthService } from "@/services/auth/AuthService"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { clineEnvConfig } from "@/config"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -49,6 +50,7 @@ export class Controller {
private disposables: vscode.Disposable[] = []
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -82,8 +84,8 @@ export class Controller {
})
}
private async getCurrentMode(): Promise<"plan" | "act"> {
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
async getCurrentMode(): Promise<Mode> {
return ((await getGlobalState(this.context, "mode")) as Mode | undefined) || "act"
}
/*
@@ -111,21 +113,20 @@ export class Controller {
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
await storeSecret(this.context, "clineAccountId", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
await updateGlobalState(this.context, "apiProvider", "openrouter")
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
])
await this.postStateToWebview()
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of Cline",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of Cline",
})
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Logout failed",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Logout failed",
})
}
}
@@ -256,153 +257,10 @@ export class Controller {
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
previousModeReasoningEffort: newReasoningEffort,
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId: newSapAiCoreModelId,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
const shouldSwitchModel = planActSeparateModelsSetting === true
if (shouldSwitchModel) {
// Save the last model used in this mode
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "vertex":
case "gemini":
case "asksage":
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
// Important we don't set modelId to this, as it's an object not string (webview expects model id to be a string)
await updateGlobalState(
this.context,
"previousModeVsCodeLmModelSelector",
apiConfiguration.vsCodeLmModelSelector,
)
break
case "openai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
case "sapaicore":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
break
}
// Restore the model used in previous mode
if (
newApiProvider ||
newModelId ||
newThinkingBudgetTokens !== undefined ||
newReasoningEffort ||
newVsCodeLmModelSelector
) {
await updateGlobalState(this.context, "apiProvider", newApiProvider)
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "vertex":
case "gemini":
case "asksage":
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "openRouterModelId", newModelId)
await updateGlobalState(this.context, "openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await updateGlobalState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
break
case "openai":
await updateGlobalState(this.context, "openAiModelId", newModelId)
await updateGlobalState(this.context, "openAiModelInfo", newModelInfo)
break
case "ollama":
await updateGlobalState(this.context, "ollamaModelId", newModelId)
break
case "lmstudio":
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
await updateGlobalState(this.context, "liteLlmModelInfo", newModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
break
case "sapaicore":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "sapAiCoreModelId", newSapAiCoreModelId)
break
}
if (this.task) {
const { apiConfiguration: updatedApiConfiguration } = await getAllExtensionState(this.context)
this.task.api = buildApiHandler(updatedApiConfiguration)
}
}
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
if (this.task) {
const { apiConfiguration } = await getAllExtensionState(this.context)
this.task.api = buildApiHandler(apiConfiguration, chatSettings.mode)
}
// Save only non-mode properties to global storage
@@ -466,30 +324,47 @@ export class Controller {
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
const clineProvider: ApiProvider = "cline"
await updateGlobalState(this.context, "apiProvider", clineProvider)
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
// Get current settings to determine how to update providers
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
if (planActSeparateModelsSetting) {
// Only update the current mode's provider
if (currentMode === "plan") {
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
} else {
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
}
} else {
// Update both modes to keep them in sync
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
])
}
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
}
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
if (this.task) {
this.task.api = buildApiHandler(updatedConfig)
this.task.api = buildApiHandler(updatedConfig, currentMode)
}
await this.postStateToWebview()
} catch (error) {
console.error("Failed to handle auth callback:", error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to log in to Cline",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to log in to Cline",
})
// Even on login failure, we preserve any existing tokens
// Only clear tokens on explicit logout
}
@@ -498,7 +373,7 @@ export class Controller {
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
const response = await axios.get(`${clineEnvConfig.mcpBaseUrl}/marketplace`, {
headers: {
"Content-Type": "application/json",
},
@@ -524,12 +399,10 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
}
return undefined
}
@@ -537,7 +410,7 @@ export class Controller {
private async fetchMcpMarketplaceFromApiRPC(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
const response = await axios.get(`${clineEnvConfig.mcpBaseUrl}/marketplace`, {
headers: {
"Content-Type": "application/json",
"User-Agent": "cline-vscode-extension",
@@ -613,12 +486,10 @@ export class Controller {
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
}
}
@@ -639,14 +510,21 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
await updateGlobalState(this.context, "apiProvider", openrouter)
const currentMode = await this.getCurrentMode()
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", openrouter),
updateGlobalState(this.context, "actModeApiProvider", openrouter),
])
await storeSecret(this.context, "openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.task) {
this.task.api = buildApiHandler({
apiProvider: openrouter,
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
openRouterApiKey: apiKey,
})
}
this.task.api = buildApiHandler(updatedConfig, currentMode)
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
@@ -905,7 +783,6 @@ export class Controller {
async clearTask() {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
await this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
@@ -982,24 +859,20 @@ export class Controller {
// Check if there's a workspace folder open
const cwd = await getCwd()
if (!cwd) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
})
return
}
// Get the git diff
const gitDiff = await getWorkingState(cwd)
if (gitDiff === "No changes in working directory") {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
})
return
}
@@ -1027,9 +900,10 @@ Commit message:`
// Get the current API configuration
const { apiConfiguration } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
// Build the API handler
const apiHandler = buildApiHandler(apiConfiguration)
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
// Create a system prompt
const systemPrompt =
@@ -1067,58 +941,46 @@ Commit message:`
const repo = api.repositories[0]
repo.inputBox.value = commitMessage
const message = "Commit message generated and applied"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
} else {
const message = "No Git repositories found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} else {
const message = "Git extension not found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} else {
const message = "Failed to generate commit message"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
}
} catch (innerError) {
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
})
}
},
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
})
}
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ import { StringRequest } from "../../../shared/proto/common"
import { McpDownloadResponse } from "../../../shared/proto/mcp"
import { McpServer } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
import { clineEnvConfig } from "@/config"
/**
* Download an MCP server from the marketplace
@@ -31,7 +31,7 @@ export async function downloadMcp(controller: Controller, request: StringRequest
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
`${clineEnvConfig.mcpBaseUrl}/download`,
{ mcpId },
{
headers: { "Content-Type": "application/json" },
@@ -0,0 +1,254 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import { getAllExtensionState } from "../../storage/state"
import { groqModels } from "../../../shared/api"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
/**
* Refreshes the Groq models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Groq models
*/
export async function refreshGroqModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
// Get the Groq API key from the controller's state
const { apiConfiguration } = await getAllExtensionState(controller.context)
const groqApiKey = apiConfiguration?.groqApiKey
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
if (!groqApiKey) {
console.log("No Groq API key found, using static models as fallback")
// Don't throw an error, just use static models
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
models[modelId] = {
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: modelInfo.description || `${modelId} model`,
}
}
} else {
// Ensure the API key is properly formatted
const cleanApiKey = groqApiKey.trim()
if (!cleanApiKey.startsWith("gsk_")) {
throw new Error("Invalid Groq API key format. Groq API keys should start with 'gsk_'")
}
console.log("Fetching Groq models with API key:", cleanApiKey.substring(0, 10) + "...")
const response = await axios.get("https://api.groq.com/openai/v1/models", {
headers: {
Authorization: `Bearer ${cleanApiKey}`,
"Content-Type": "application/json",
"User-Agent": "Cline-VSCode-Extension",
},
timeout: 10000, // 10 second timeout
})
if (response.data?.data) {
const rawModels = response.data.data
for (const rawModel of rawModels) {
// Filter out non-chat models and validate model capabilities
if (!isValidChatModel(rawModel)) {
continue
}
// Check if we have static pricing information for this model
const staticModelInfo = groqModels[rawModel.id as keyof typeof groqModels]
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens || 8192,
contextWindow: rawModel.context_window || staticModelInfo?.contextWindow || 8192,
supportsImages: detectImageSupport(rawModel, staticModelInfo),
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
inputPrice: staticModelInfo?.inputPrice || 0,
outputPrice: staticModelInfo?.outputPrice || 0,
cacheWritesPrice: (staticModelInfo as any)?.cacheWritesPrice || 0,
cacheReadsPrice: (staticModelInfo as any).cacheReadsPrice || 0,
description: generateModelDescription(rawModel, staticModelInfo),
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from Groq API")
}
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
console.log("Groq models fetched and saved", models)
}
} catch (error) {
console.error("Error fetching Groq models:", error)
// Provide more specific error messages
let errorMessage = "Unknown error occurred"
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
errorMessage = "Invalid Groq API key. Please check your API key in settings."
} else if (error.response?.status === 403) {
errorMessage = "Access forbidden. Please verify your Groq API key has the correct permissions."
} else if (error.response?.status === 429) {
errorMessage = "Rate limit exceeded. Please try again later."
} else if (error.code === "ECONNABORTED") {
errorMessage = "Request timeout. Please check your internet connection."
} else {
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
}
} else if (error instanceof Error) {
errorMessage = error.message
}
console.error("Groq API Error:", errorMessage)
// If we failed to fetch models, try to read cached models first
const cachedModels = await readGroqModels(controller)
if (cachedModels && Object.keys(cachedModels).length > 0) {
console.log("Using cached Groq models")
models = cachedModels
} else {
// Fall back to static models from shared/api.ts
console.log("Using static Groq models as fallback")
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
models[modelId] = {
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: modelInfo.description || `${modelId} model`,
}
}
}
}
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 8192,
contextWindow: model.contextWindow ?? 8192,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
tiers: model.tiers ?? [],
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached Groq models from disk
*/
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
const fileExists = await fileExistsAtPath(groqModelsFilePath)
if (fileExists) {
try {
const fileContents = await fs.readFile(groqModelsFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
console.error("Error reading cached Groq models:", error)
return undefined
}
}
return undefined
}
/**
* Validates if a model is suitable for chat completions
*/
function isValidChatModel(rawModel: any): boolean {
// Check if model is active (if the property exists)
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
return false
}
// Filter out non-chat models (whisper, TTS, guard models, etc.)
if (
rawModel.id.includes("whisper") ||
rawModel.id.includes("tts") ||
rawModel.id.includes("guard") ||
rawModel.id.includes("embedding") ||
rawModel.id.includes("moderation") ||
rawModel.id.includes("allam")
) {
return false
}
// Check if model supports chat completions
if (rawModel.object === "model" && rawModel.id) {
return true
}
return false
}
/**
* Detects if a model supports image input
*/
function detectImageSupport(rawModel: any, staticModelInfo?: any): boolean {
// Use static info if available
if (staticModelInfo?.supportsImages !== undefined) {
return staticModelInfo.supportsImages
}
// Detect based on model name patterns
const modelId = rawModel.id.toLowerCase()
if (modelId.includes("vision") || modelId.includes("maverick") || modelId.includes("scout")) {
return true
}
return false
}
/**
* Generates a descriptive name for the model
*/
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
// Use static description if available
if (staticModelInfo?.description) {
return staticModelInfo.description
}
// Generate description based on model characteristics
const modelId = rawModel.id
const contextWindow = rawModel.context_window || 8192
const ownedBy = rawModel.owned_by || "Unknown"
// Special handling for new models
if (modelId.includes("compound")) {
return `${ownedBy}'s ${modelId} model with ${contextWindow.toLocaleString()} token context window - Advanced compound architecture`
}
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -0,0 +1,112 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
import { huggingFaceModels } from "@shared/api"
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
try {
await fs.mkdir(cacheDir, { recursive: true })
} catch (error) {
// Directory might already exist
}
return cacheDir
}
/**
* Refreshes the Hugging Face models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Hugging Face models
*/
export async function refreshHuggingFaceModels(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
let models: Record<string, OpenRouterModelInfo> = {}
try {
// Fetch models from Hugging Face API
const response = await axios.get("https://router.huggingface.co/v1/models", {
timeout: 10000,
})
if (response.data?.data) {
const rawModels = response.data.data
// Transform HF models to OpenRouter-compatible format
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: 8192, // HF doesn't provide max_tokens, use default
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
supportsImages: false, // Most models don't support images
supportsPromptCache: false,
inputPrice: 0, // Will be set based on providers
outputPrice: 0, // Will be set based on providers
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
})
// Add model-specific configurations if we have them in our static models
if (rawModel.id in huggingFaceModels) {
const staticModel = huggingFaceModels[rawModel.id as keyof typeof huggingFaceModels]
modelInfo.maxTokens = staticModel.maxTokens
modelInfo.contextWindow = staticModel.contextWindow
modelInfo.supportsImages = staticModel.supportsImages
modelInfo.supportsPromptCache = staticModel.supportsPromptCache
modelInfo.inputPrice = staticModel.inputPrice
modelInfo.outputPrice = staticModel.outputPrice
modelInfo.description = staticModel.description || modelInfo.description
}
models[rawModel.id] = modelInfo
}
// Save to cache
await fs.writeFile(huggingFaceModelsFilePath, JSON.stringify(models, null, 2))
}
} catch (error) {
console.error("Error fetching Hugging Face models:", error)
// Try to load from cache
try {
if (await fileExistsAtPath(huggingFaceModelsFilePath)) {
const cachedModels = await fs.readFile(huggingFaceModelsFilePath, "utf-8")
const parsedModels = JSON.parse(cachedModels)
models = parsedModels
}
} catch (cacheError) {
console.error("Error loading cached Hugging Face models:", cacheError)
}
// If no cache available, use static models as fallback
if (Object.keys(models).length === 0) {
for (const [modelId, modelInfo] of Object.entries(huggingFaceModels)) {
models[modelId] = OpenRouterModelInfo.create({
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: modelInfo.description || "",
})
}
}
}
return OpenRouterCompatibleModelInfo.create({ models })
}
@@ -29,7 +29,8 @@ export async function updateApiConfigurationProto(
// Update the task's API handler if there's an active task
if (controller.task) {
controller.task.api = buildApiHandler(appApiConfiguration)
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(appApiConfiguration, currentMode)
}
// Post updated state to webview
+16 -24
View File
@@ -15,20 +15,16 @@ import { getHostBridgeProvider } from "@/hosts/host-providers"
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
try {
if (request.global) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting global state...",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Resetting global state...",
})
await resetGlobalState(controller.context)
} else {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting workspace state...",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Resetting workspace state...",
})
await resetWorkspaceState(controller.context)
}
@@ -37,12 +33,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
controller.task = undefined
}
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "State reset",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "State reset",
})
await controller.postStateToWebview()
await sendChatButtonClickedEvent(controller.id)
@@ -50,12 +44,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
return Empty.create()
} catch (error) {
console.error("Error resetting state:", error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
})
throw error
}
}
@@ -25,7 +25,7 @@ export async function subscribeToState(
const initialState = await controller.getStateToPostToWebview()
const initialStateJson = JSON.stringify(initialState)
console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
//console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
await responseStream({
stateJson: initialStateJson,
@@ -37,7 +37,7 @@ export async function subscribeToState(
// Register cleanup when the connection is closed
const cleanup = () => {
activeStateSubscriptions.delete(controllerId)
console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
//console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
}
// Register the cleanup function with the request registry if we have a requestId
@@ -68,7 +68,7 @@ export async function sendStateUpdate(controllerId: string, state: any): Promise
},
false, // Not the last message
)
console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
//console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
} catch (error) {
console.error(`Error sending state update to controller ${controllerId}:`, error)
// Remove the subscription if there was an error
@@ -27,12 +27,10 @@ export async function updateDefaultTerminalProfile(
// Show information message if terminals were closed
if (closedCount > 0) {
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
}
// Show warning if there are busy terminals that couldn't be closed
@@ -40,12 +38,10 @@ export async function updateDefaultTerminalProfile(
const message =
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
})
}
}
+2 -1
View File
@@ -21,7 +21,8 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await updateApiConfiguration(controller.context, apiConfiguration)
if (controller.task) {
controller.task.api = buildApiHandler(apiConfiguration)
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(apiConfiguration, currentMode)
}
}
@@ -33,7 +33,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
},
}),
)
)?.selectedOption
).selectedOption
// Default VS Code Cancel button returns `undefined` - don't delete anything
if (userChoice === undefined) {
@@ -67,17 +67,15 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
} else {
// No favorited tasks found - show warning and ask user what to do
const answer = (
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
options: {
modal: true,
items: ["Delete All Tasks"],
},
}),
)
)?.selectedOption
await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
options: {
modal: true,
items: ["Delete All Tasks"],
},
})
).selectedOption
// User cancelled - don't delete anything
if (answer === undefined) {
@@ -105,12 +103,10 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
}
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
})
}
// Update webview
@@ -28,13 +28,11 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
? "Are you sure you want to delete this task? This action cannot be undone."
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
options: { modal: true, items: ["Delete"] },
}),
)
const userChoice = await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
options: { modal: true, items: ["Delete"] },
})
if (userChoice === undefined) {
return Empty.create()
+71 -8
View File
@@ -30,14 +30,77 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
handleModelsServiceRequest(controller, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await controller.postStateToWebview()
const { apiConfiguration, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
const currentMode = await controller.getCurrentMode()
if (planActSeparateModelsSetting) {
// Separate models: update only current mode
const modelIdField = currentMode === "plan" ? "planModeOpenRouterModelId" : "actModeOpenRouterModelId"
const modelInfoField = currentMode === "plan" ? "planModeOpenRouterModelInfo" : "actModeOpenRouterModelInfo"
const modelId = apiConfiguration[modelIdField]
if (modelId && response.models[modelId]) {
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
await controller.postStateToWebview()
}
} else {
// Shared models: update both plan and act modes
const planModelId = apiConfiguration.planModeOpenRouterModelId
const actModelId = apiConfiguration.actModeOpenRouterModelId
// Update plan mode model info if we have a model ID
if (planModelId && response.models[planModelId]) {
await updateGlobalState(controller.context, "planModeOpenRouterModelInfo", response.models[planModelId])
}
// Update act mode model info if we have a model ID
if (actModelId && response.models[actModelId]) {
await updateGlobalState(controller.context, "actModeOpenRouterModelInfo", response.models[actModelId])
}
// Post state update if we updated any model info
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
await controller.postStateToWebview()
}
}
}
})
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// Update model info in state for Groq (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
const currentMode = await controller.getCurrentMode()
if (planActSeparateModelsSetting) {
// Separate models: update only current mode
const modelIdField = currentMode === "plan" ? "planModeGroqModelId" : "actModeGroqModelId"
const modelInfoField = currentMode === "plan" ? "planModeGroqModelInfo" : "actModeGroqModelInfo"
const modelId = apiConfiguration[modelIdField]
if (modelId && response.models[modelId]) {
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
await controller.postStateToWebview()
}
} else {
// Shared models: update both plan and act modes
const planModelId = apiConfiguration.planModeGroqModelId
const actModelId = apiConfiguration.actModeGroqModelId
// Update plan mode model info if we have a model ID
if (planModelId && response.models[planModelId]) {
await updateGlobalState(controller.context, "planModeGroqModelInfo", response.models[planModelId])
}
// Update act mode model info if we have a model ID
if (actModelId && response.models[actModelId]) {
await updateGlobalState(controller.context, "actModeGroqModelInfo", response.models[actModelId])
}
// Post state update if we updated any model info
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
await controller.postStateToWebview()
}
}
}
})
+22
View File
@@ -0,0 +1,22 @@
import * as vscode from "vscode"
import type { Controller } from "../index"
import type { EmptyRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
/**
* Opens the Cline walkthrough in VSCode
* @param controller The controller instance
* @param request Empty request
* @returns Empty response
*/
export async function openWalkthrough(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
telemetryService.captureButtonClick("webview_openWalkthrough")
return Empty.create({})
} catch (error) {
console.error(`Failed to open walkthrough: ${error}`)
throw error
}
}
+8 -12
View File
@@ -78,12 +78,10 @@ export async function parseMentions(
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${urlMention}: ${error.message}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${urlMention}: ${error.message}`,
})
}
}
@@ -100,12 +98,10 @@ export async function parseMentions(
const markdown = await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${mention}: ${error.message}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${mention}: ${error.message}`,
})
result = `Error fetching content: ${error.message}`
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as diff from "diff"
import * as path from "path"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
import { Mode } from "@/shared/ChatSettings"
export const formatResponse = {
duplicateFileReadNotice: () =>
@@ -147,7 +148,7 @@ Otherwise, if you have not completed the task and do not need additional informa
},
taskResumption: (
mode: "plan" | "act",
mode: Mode,
agoText: string,
cwd: string,
wasRecent: boolean | 0 | undefined,
+1
View File
@@ -13,6 +13,7 @@ export const GlobalFileNames = {
contextHistory: "context_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
groqModels: "groq_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
workflows: ".clinerules/workflows",
+52 -34
View File
@@ -21,11 +21,13 @@ export type SecretKey =
| "asksageApiKey"
| "xaiApiKey"
| "moonshotApiKey"
| "huggingFaceApiKey"
| "nebiusApiKey"
| "sambanovaApiKey"
| "cerebrasApiKey"
| "sapAiCoreClientId"
| "sapAiCoreClientSecret"
| "groqApiKey"
export type GlobalStateKey =
| "awsRegion"
@@ -41,12 +43,9 @@ export type GlobalStateKey =
| "lastShownAnnouncementId"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "openAiHeaders"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "geminiBaseUrl"
@@ -86,36 +85,55 @@ export type GlobalStateKey =
// Settings around plan/act and ephemeral model configuration
| "chatSettings"
| "mode"
// Current active model configuration (per workspace)
| "apiProvider"
| "apiModelId"
| "thinkingBudgetTokens"
| "reasoningEffort"
| "vsCodeLmModelSelector"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "openRouterModelId"
| "openRouterModelInfo"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "lmStudioModelId"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "fireworksModelId"
| "sapAiCoreModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "previousModeVsCodeLmModelSelector"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeSapAiCoreModelId"
// Plan mode configurations
| "planModeApiProvider"
| "planModeApiModelId"
| "planModeThinkingBudgetTokens"
| "planModeReasoningEffort"
| "planModeVsCodeLmModelSelector"
| "planModeAwsBedrockCustomSelected"
| "planModeAwsBedrockCustomModelBaseId"
| "planModeOpenRouterModelId"
| "planModeOpenRouterModelInfo"
| "planModeOpenAiModelId"
| "planModeOpenAiModelInfo"
| "planModeOllamaModelId"
| "planModeLmStudioModelId"
| "planModeLiteLlmModelId"
| "planModeLiteLlmModelInfo"
| "planModeRequestyModelId"
| "planModeRequestyModelInfo"
| "planModeTogetherModelId"
| "planModeFireworksModelId"
| "planModeSapAiCoreModelId"
| "planModeGroqModelId"
| "planModeGroqModelInfo"
| "planModeHuggingFaceModelId"
| "planModeHuggingFaceModelInfo"
// Act mode configurations
| "actModeApiProvider"
| "actModeApiModelId"
| "actModeThinkingBudgetTokens"
| "actModeReasoningEffort"
| "actModeVsCodeLmModelSelector"
| "actModeAwsBedrockCustomSelected"
| "actModeAwsBedrockCustomModelBaseId"
| "actModeOpenRouterModelId"
| "actModeOpenRouterModelInfo"
| "actModeOpenAiModelId"
| "actModeOpenAiModelInfo"
| "actModeOllamaModelId"
| "actModeLmStudioModelId"
| "actModeLiteLlmModelId"
| "actModeLiteLlmModelInfo"
| "actModeRequestyModelId"
| "actModeRequestyModelInfo"
| "actModeTogetherModelId"
| "actModeFireworksModelId"
| "actModeSapAiCoreModelId"
| "actModeGroqModelId"
| "actModeGroqModelInfo"
| "actModeHuggingFaceModelId"
| "actModeHuggingFaceModelInfo"
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
+381 -5
View File
@@ -32,6 +32,10 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
"togetherModelId",
"fireworksModelId",
"sapAiCoreModelId",
"groqModelId",
"groqModelInfo",
"huggingFaceModelId",
"huggingFaceModelInfo",
// Previous mode settings
"previousModeApiProvider",
@@ -53,8 +57,8 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
if (workspaceValue !== undefined && globalValue === undefined) {
console.log(`[Storage Migration] migrating key: ${key} to global storage. Current value: ${workspaceValue}`)
// Move to global storage
await updateGlobalState(context, key as GlobalStateKey, workspaceValue)
// Move to global storage using raw VSCode method to avoid type errors
await context.globalState.update(key, workspaceValue)
// Remove from workspace storage
await context.workspaceState.update(key, undefined)
const newWorkspaceValue = await context.workspaceState.get(key)
@@ -169,6 +173,375 @@ export async function migrateModeFromWorkspaceStorageToControllerState(context:
}
}
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
try {
// Check if migration is needed - if planModeApiProvider already exists, skip migration
const planModeApiProvider = await context.globalState.get("planModeApiProvider")
if (planModeApiProvider !== undefined) {
console.log("Legacy API configuration migration already completed, skipping...")
return
}
console.log("Starting legacy API configuration migration to mode-specific keys...")
// Get the planActSeparateModelsSetting to determine migration strategy
const planActSeparateModelsSetting = (await context.globalState.get("planActSeparateModelsSetting")) as
| boolean
| undefined
// Read legacy values directly
const apiProvider = await context.globalState.get("apiProvider")
const apiModelId = await context.globalState.get("apiModelId")
const thinkingBudgetTokens = await context.globalState.get("thinkingBudgetTokens")
const reasoningEffort = await context.globalState.get("reasoningEffort")
const vsCodeLmModelSelector = await context.globalState.get("vsCodeLmModelSelector")
const awsBedrockCustomSelected = await context.globalState.get("awsBedrockCustomSelected")
const awsBedrockCustomModelBaseId = await context.globalState.get("awsBedrockCustomModelBaseId")
const openRouterModelId = await context.globalState.get("openRouterModelId")
const openRouterModelInfo = await context.globalState.get("openRouterModelInfo")
const openAiModelId = await context.globalState.get("openAiModelId")
const openAiModelInfo = await context.globalState.get("openAiModelInfo")
const ollamaModelId = await context.globalState.get("ollamaModelId")
const lmStudioModelId = await context.globalState.get("lmStudioModelId")
const liteLlmModelId = await context.globalState.get("liteLlmModelId")
const liteLlmModelInfo = await context.globalState.get("liteLlmModelInfo")
const requestyModelId = await context.globalState.get("requestyModelId")
const requestyModelInfo = await context.globalState.get("requestyModelInfo")
const togetherModelId = await context.globalState.get("togetherModelId")
const fireworksModelId = await context.globalState.get("fireworksModelId")
const sapAiCoreModelId = await context.globalState.get("sapAiCoreModelId")
const groqModelId = await context.globalState.get("groqModelId")
const groqModelInfo = await context.globalState.get("groqModelInfo")
const huggingFaceModelId = await context.globalState.get("huggingFaceModelId")
const huggingFaceModelInfo = await context.globalState.get("huggingFaceModelInfo")
// Read previous mode values
const previousModeApiProvider = await context.globalState.get("previousModeApiProvider")
const previousModeModelId = await context.globalState.get("previousModeModelId")
const previousModeModelInfo = await context.globalState.get("previousModeModelInfo")
const previousModeVsCodeLmModelSelector = await context.globalState.get("previousModeVsCodeLmModelSelector")
const previousModeThinkingBudgetTokens = await context.globalState.get("previousModeThinkingBudgetTokens")
const previousModeReasoningEffort = await context.globalState.get("previousModeReasoningEffort")
const previousModeAwsBedrockCustomSelected = await context.globalState.get("previousModeAwsBedrockCustomSelected")
const previousModeAwsBedrockCustomModelBaseId = await context.globalState.get("previousModeAwsBedrockCustomModelBaseId")
const previousModeSapAiCoreModelId = await context.globalState.get("previousModeSapAiCoreModelId")
// Migrate based on planActSeparateModelsSetting
if (planActSeparateModelsSetting === false) {
console.log("Migrating with separate models DISABLED - using current values for both modes")
// Use current values for both plan and act modes
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
} else {
console.log("Migrating with separate models ENABLED - using current->plan, previous->act")
// Use current values for plan mode
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
// Use previous values for act mode (with fallback to current values)
if (previousModeApiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", previousModeApiProvider)
} else if (apiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (previousModeModelId !== undefined) {
await context.globalState.update("actModeApiModelId", previousModeModelId)
} else if (apiModelId !== undefined) {
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (previousModeThinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", previousModeThinkingBudgetTokens)
} else if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (previousModeReasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", previousModeReasoningEffort)
} else if (reasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (previousModeVsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", previousModeVsCodeLmModelSelector)
} else if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (previousModeAwsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", previousModeAwsBedrockCustomSelected)
} else if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (previousModeAwsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", previousModeAwsBedrockCustomModelBaseId)
} else if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (previousModeSapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", previousModeSapAiCoreModelId)
} else if (sapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
// For fields without previous variants, use current values for act mode
if (previousModeModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", previousModeModelInfo)
} else if (openRouterModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openAiModelId !== undefined) {
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
}
// Clean up legacy keys after successful migration
console.log("Cleaning up legacy keys...")
await context.globalState.update("apiProvider", undefined)
await context.globalState.update("apiModelId", undefined)
await context.globalState.update("thinkingBudgetTokens", undefined)
await context.globalState.update("reasoningEffort", undefined)
await context.globalState.update("vsCodeLmModelSelector", undefined)
await context.globalState.update("awsBedrockCustomSelected", undefined)
await context.globalState.update("awsBedrockCustomModelBaseId", undefined)
await context.globalState.update("openRouterModelId", undefined)
await context.globalState.update("openRouterModelInfo", undefined)
await context.globalState.update("openAiModelId", undefined)
await context.globalState.update("openAiModelInfo", undefined)
await context.globalState.update("ollamaModelId", undefined)
await context.globalState.update("lmStudioModelId", undefined)
await context.globalState.update("liteLlmModelId", undefined)
await context.globalState.update("liteLlmModelInfo", undefined)
await context.globalState.update("requestyModelId", undefined)
await context.globalState.update("requestyModelInfo", undefined)
await context.globalState.update("togetherModelId", undefined)
await context.globalState.update("fireworksModelId", undefined)
await context.globalState.update("sapAiCoreModelId", undefined)
await context.globalState.update("groqModelId", undefined)
await context.globalState.update("groqModelInfo", undefined)
await context.globalState.update("huggingFaceModelId", undefined)
await context.globalState.update("huggingFaceModelInfo", undefined)
await context.globalState.update("previousModeApiProvider", undefined)
await context.globalState.update("previousModeModelId", undefined)
await context.globalState.update("previousModeModelInfo", undefined)
await context.globalState.update("previousModeVsCodeLmModelSelector", undefined)
await context.globalState.update("previousModeThinkingBudgetTokens", undefined)
await context.globalState.update("previousModeReasoningEffort", undefined)
await context.globalState.update("previousModeAwsBedrockCustomSelected", undefined)
await context.globalState.update("previousModeAwsBedrockCustomModelBaseId", undefined)
await context.globalState.update("previousModeSapAiCoreModelId", undefined)
console.log("Successfully migrated legacy API configuration to mode-specific keys")
} catch (error) {
console.error("Failed to migrate legacy API configuration to mode-specific keys:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
@@ -190,8 +563,10 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.planModeOllamaModelId,
config.planModeLmStudioModelId,
config.actModeOllamaModelId,
config.actModeLmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
@@ -201,7 +576,8 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.planModeVsCodeLmModelSelector,
config.actModeVsCodeLmModelSelector,
config.clineAccountId,
config.asksageApiKey,
config.xaiApiKey,
+266 -133
View File
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_CHAT_SETTINGS, Mode } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
@@ -167,8 +167,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
huggingFaceApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
@@ -244,8 +246,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
getSecret(context, "groqApiKey") as Promise<string | undefined>,
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
@@ -273,73 +277,115 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
chatSettings,
currentMode,
storedApiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
sapAiCoreModelId,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
] = await Promise.all([
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "mode") as Promise<Mode | undefined>,
// Plan mode configurations
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "planModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "planModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "planModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "planModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "planModeOpenRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeOpenAiModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeOllamaModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLiteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeRequestyModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeTogetherModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeFireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeGroqModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeGroqModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeHuggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
// Act mode configurations
getGlobalState(context, "actModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "actModeApiModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "actModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "actModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "actModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "actModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "actModeOpenRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeOpenAiModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeOllamaModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLiteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeRequestyModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeTogetherModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeFireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeGroqModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeGroqModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeHuggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
])
const processingStart = performance.now()
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
if (planModeApiProvider) {
apiProvider = planModeApiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
@@ -362,7 +408,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (storedApiProvider) {
if (planModeApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
@@ -375,8 +421,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
return {
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineAccountId,
@@ -392,19 +436,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockApiKey,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
openAiHeaders: openAiHeaders || {},
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
@@ -412,29 +450,18 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
moonshotApiLine,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
thinkingBudgetTokens,
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
asksageApiKey,
@@ -442,6 +469,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
@@ -451,7 +479,55 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
huggingFaceApiKey,
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider: actModeApiProvider || apiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
@@ -467,15 +543,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
mode: currentMode || "act", // Merge mode from global state
},
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
mcpResponsesCollapsed: mcpResponsesCollapsed,
@@ -492,8 +559,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
@@ -507,19 +572,13 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsProfile,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
openAiHeaders,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
@@ -527,21 +586,13 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
@@ -549,16 +600,14 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
clineAccountId,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
sapAiCoreClientId,
@@ -566,33 +615,113 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
huggingFaceApiKey,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
} = apiConfiguration
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
const batchedGlobalUpdates = {
// Ephemeral model config updates (20 keys)
apiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
sapAiCoreModelId,
// Plan mode configuration updates
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configuration updates
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
// Global state updates (27 keys)
awsRegion,
@@ -652,10 +781,12 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
huggingFaceApiKey,
}
// Execute batched operations in parallel for maximum performance
@@ -696,8 +827,10 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"xaiApiKey",
"sambanovaApiKey",
"cerebrasApiKey",
"groqApiKey",
"moonshotApiKey",
"nebiusApiKey",
"huggingFaceApiKey",
]
for (const key of secretKeys) {
await storeSecret(context, key, undefined)
+9 -2
View File
@@ -54,6 +54,7 @@ import { TaskState } from "./TaskState"
import { MessageStateHandler } from "./message-state"
import { AutoApprove } from "./tools/autoApprove"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
import { ChatSettings } from "@/shared/ChatSettings"
export class ToolExecutor {
private autoApprover: AutoApprove
@@ -90,6 +91,7 @@ export class ToolExecutor {
private browserSettings: BrowserSettings,
private cwd: string,
private taskId: string,
private chatSettings: ChatSettings,
// Callbacks to the Task (Entity)
private say: (
@@ -634,7 +636,7 @@ export class ToolExecutor {
}
await this.diffViewProvider.update(newContent, true)
await setTimeoutPromise(300) // wait for diff view to update
this.diffViewProvider.scrollToFirstDiff()
await this.diffViewProvider.scrollToFirstDiff()
// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
const completeMessage = JSON.stringify({
@@ -1917,7 +1919,12 @@ export class ToolExecutor {
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const providerAndModel = `${await getGlobalState(this.context, "apiProvider")} / ${this.api.getModel().id}`
const currentMode = this.chatSettings.mode
const apiProvider =
currentMode === "plan"
? await getGlobalState(this.context, "planModeApiProvider")
: await getGlobalState(this.context, "actModeApiProvider")
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
+87 -53
View File
@@ -81,9 +81,10 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
import { updateApiReqMsg } from "./utils"
import { createDiffViewProvider } from "@/hosts/host-providers"
import { ErrorService } from "@/services/error/ErrorService"
import { ClineErrorType } from "@/services/error/ClineError"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
@@ -165,8 +166,19 @@ export class Task {
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
// Initialization moved to startTask/resumeTaskFromHistory
this.terminalManager = new TerminalManager()
// TODO(ae) this is a hack to replace the terminal manager for standalone,
// until we have proper host bridge support for terminal execution. The
// standaloneTerminalManager is defined in the vscode-impls and injected
// during compilation of the standalone manager only, so this variable only
// exists in that case
if ((global as any).standaloneTerminalManager) {
console.log("[DEBUG] Using vscode-impls.js terminal manager")
this.terminalManager = (global as any).standaloneTerminalManager
} else {
console.log("[DEBUG] Using built in terminal manager")
this.terminalManager = new TerminalManager()
}
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
@@ -249,12 +261,19 @@ export class Task {
},
}
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
const currentProvider =
chatSettings.mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
if (currentProvider === "openai" || currentProvider === "openai-native") {
if (chatSettings.mode === "plan") {
effectiveApiConfiguration.planModeReasoningEffort = chatSettings.openAIReasoningEffort
} else {
effectiveApiConfiguration.actModeReasoningEffort = chatSettings.openAIReasoningEffort
}
}
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler(effectiveApiConfiguration)
this.api = buildApiHandler(effectiveApiConfiguration, chatSettings.mode)
// Set taskId on browserSession for telemetry tracking
this.browserSession.setTaskId(this.taskId)
@@ -269,10 +288,10 @@ export class Task {
// initialize telemetry
if (historyItem) {
// Open task from history
telemetryService.captureTaskRestarted(this.taskId, apiConfiguration.apiProvider)
telemetryService.captureTaskRestarted(this.taskId, currentProvider)
} else {
// New task started
telemetryService.captureTaskCreated(this.taskId, apiConfiguration.apiProvider)
telemetryService.captureTaskCreated(this.taskId, currentProvider)
}
this.toolExecutor = new ToolExecutor(
@@ -292,6 +311,7 @@ export class Task {
this.browserSettings,
cwd,
this.taskId,
this.chatSettings,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
@@ -316,7 +336,17 @@ export class Task {
* Updates the auto approval settings for this task
*/
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
// Check if maxRequests changed
const maxRequestsChanged = this.autoApprovalSettings.maxRequests !== settings.maxRequests
// Update the settings
this.autoApprovalSettings = settings
this.toolExecutor.updateAutoApprovalSettings(settings)
// Reset counter if max requests limit changed
if (maxRequestsChanged) {
this.taskState.consecutiveAutoApprovedRequestsCount = 0
}
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
@@ -1567,6 +1597,15 @@ export class Task {
}
}
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
const modelId = this.api.getModel()?.id
const providerId =
this.chatSettings.mode === "plan"
? ((await getGlobalState(this.getContext(), "planModeApiProvider")) as string)
: ((await getGlobalState(this.getContext(), "actModeApiProvider")) as string)
return { modelId, providerId }
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@@ -1661,16 +1700,17 @@ export class Task {
const isAnthropic = this.api instanceof AnthropicHandler
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
const { modelId, providerId } = await this.getCurrentProviderInfo()
const clineError = ErrorService.toClineError(error, modelId, providerId)
const { statusCode, message, requestId } = extractErrorDetails(error)
// Capture provider failure telemetry
// Capture provider failure telemetry using clineError
// TODO: Move into ErrorService
telemetryService.captureProviderApiError({
taskId: this.taskId,
model: modelInfo.id,
errorMessage: message,
errorStatus: statusCode,
requestId,
errorMessage: clineError.message,
errorStatus: clineError._error?.status,
requestId: clineError._error?.request_id,
})
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
@@ -1716,12 +1756,12 @@ export class Task {
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
// ToDo: Allow the user to change their input if this is the case.
if (truncatedConversationHistory.length > 3) {
error = new Error("Context window exceeded. Click retry to truncate the conversation and try again.")
clineError.message = "Context window exceeded. Click retry to truncate the conversation and try again."
this.taskState.didAutomaticallyRetryFailedApiRequest = false
}
}
const errorMessage = formatErrorWithStatusCode(error)
const streamingFailedMessage = clineError.serialize()
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
const lastApiReqStartedIndex = findLastIndex(
@@ -1737,19 +1777,24 @@ export class Task {
text: JSON.stringify({
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
streamingFailedMessage: errorMessage,
streamingFailedMessage,
} satisfies ClineApiReqInfo),
})
// this.ask will trigger postStateToWebview, so this change should be picked up.
}
const { response } = await this.ask("api_req_failed", errorMessage)
const { response } = await this.ask("api_req_failed", streamingFailedMessage)
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
throw new Error("API request failed")
}
// Do not retry automatically again if currently unauthenticated
if (clineError.isErrorType(ClineErrorType.Auth)) {
return
}
await this.say("api_req_retried")
}
// delegate generator output from the recursive call
@@ -1885,10 +1930,10 @@ export class Task {
}
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
if (currentProviderId && this.api.getModel().id) {
const { modelId, providerId } = await this.getCurrentProviderInfo()
if (providerId && modelId) {
try {
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.chatSettings.mode)
} catch {}
}
@@ -2066,7 +2111,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user", true)
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "user")
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
@@ -2131,20 +2176,13 @@ export class Task {
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
telemetryService.captureConversationTurnEvent(this.taskId, providerId, this.api.getModel().id, "assistant", {
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.taskState.didFinishAbortingStream = true
@@ -2241,7 +2279,8 @@ export class Task {
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
if (!this.taskState.abandoned) {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
const errorMessage = formatErrorWithStatusCode(error)
const clineError = ErrorService.toClineError(error, this.api.getModel().id)
const errorMessage = clineError.serialize()
await abortStream("streaming_failed", errorMessage)
await this.reinitExistingTaskFromId(this.taskId)
@@ -2311,20 +2350,13 @@ export class Task {
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "assistant", {
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
await this.messageStateHandler.addToApiConversationHistory({
role: "assistant",
@@ -2370,6 +2402,8 @@ export class Task {
},
],
})
// Returns early to avoid retry since no assistant message was received
return true
}
return didEndLoop // will always be false for now
-16
View File
@@ -1,25 +1,9 @@
import { showSystemNotification } from "@/integrations/notifications"
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
import { serializeError } from "serialize-error"
import { MessageStateHandler } from "./message-state"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { ApiHandler } from "@/api"
export function formatErrorWithStatusCode(error: any): string {
const { statusCode, message } = extractErrorDetails(error)
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
const requestId = error.request_id || error.response?.request_id || undefined
return { message, statusCode, requestId }
}
export const showNotificationForApprovalIfAutoApprovalEnabled = (
message: string,
autoApprovalSettingsEnabled: boolean,
+5 -4
View File
@@ -262,13 +262,14 @@ export abstract class WebviewProvider {
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
// Only show the error message if not in development mode.
if (!process.env.IS_DEV) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
}),
)
})
}
return this.getHtmlContent()
}
+4 -6
View File
@@ -99,12 +99,10 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
await controller.postStateToWebview()
const message = `Created ${tasksCount} test tasks`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
},
)
}),
+41 -40
View File
@@ -27,6 +27,7 @@ import {
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
migrateLegacyApiConfigurationToModeSpecific,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -75,6 +76,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate workspace storage values back to global storage (reverting previous migration)
await migrateWorkspaceToGlobalStorage(context)
// Migrate legacy API configuration to mode-specific keys (one-time migration)
await migrateLegacyApiConfigurationToModeSpecific(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
@@ -106,12 +110,10 @@ export async function activate(context: vscode.ExtensionContext) {
const message = `Cline has been updated to v${currentVersion}`
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await new Promise((resolve) => setTimeout(resolve, 200))
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
// Record that we've shown the popup for this version.
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
}
@@ -363,7 +365,7 @@ export async function activate(context: vscode.ExtensionContext) {
languageId,
Array.isArray(diagnostics) ? diagnostics : undefined,
)
telemetryService.captureButtonClick("codeAction_addToChat", visibleWebview?.controller.task?.taskId, true)
telemetryService.captureButtonClick("codeAction_addToChat", visibleWebview?.controller.task?.taskId)
}),
)
@@ -413,12 +415,10 @@ export async function activate(context: vscode.ExtensionContext) {
// Ensure clipboard is restored even if an error occurs
await writeTextToClipboard(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to get terminal contents",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to get terminal contents",
})
}
}),
)
@@ -540,7 +540,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Send to sidebar provider with diagnostics
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
telemetryService.captureButtonClick("codeAction_fixWithCline", visibleWebview?.controller.task?.taskId, true)
telemetryService.captureButtonClick("codeAction_fixWithCline", visibleWebview?.controller.task?.taskId)
}),
)
@@ -554,12 +554,10 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to explain.",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Please select some code to explain.",
})
return
}
const filePath = editor.document.uri.fsPath
@@ -567,7 +565,7 @@ export async function activate(context: vscode.ExtensionContext) {
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
const prompt = `Explain the following code from ${fileMention}:\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
await visibleWebview?.controller.initTask(prompt)
telemetryService.captureButtonClick("codeAction_explainCode", visibleWebview?.controller.task?.taskId, true)
telemetryService.captureButtonClick("codeAction_explainCode", visibleWebview?.controller.task?.taskId)
}),
)
@@ -581,12 +579,10 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to improve.",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Please select some code to improve.",
})
return
}
const filePath = editor.document.uri.fsPath
@@ -594,7 +590,7 @@ export async function activate(context: vscode.ExtensionContext) {
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
const prompt = `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
await visibleWebview?.controller.initTask(prompt)
telemetryService.captureButtonClick("codeAction_improveCode", visibleWebview?.controller.task?.taskId, true)
telemetryService.captureButtonClick("codeAction_improveCode", visibleWebview?.controller.task?.taskId)
}),
)
@@ -651,14 +647,12 @@ export async function activate(context: vscode.ExtensionContext) {
sendFocusChatInputEvent(clientId)
} else {
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
})
}
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId, true)
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
}),
)
@@ -666,7 +660,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.openWalkthrough", async () => {
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
telemetryService.captureButtonClick("command_openWalkthrough", undefined, true)
telemetryService.captureButtonClick("command_openWalkthrough")
}),
)
@@ -691,9 +685,18 @@ export async function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(
context.secrets.onDidChange((event) => {
context.secrets.onDidChange(async (event) => {
if (event.key === "clineAccountId") {
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
// Check if the secret was removed (logout) or added/updated (login)
const secretValue = await context.secrets.get("clineAccountId")
const authService = AuthService.getInstance(context)
if (secretValue) {
// Secret was added or updated - restore auth info (login from another window)
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
} else {
// Secret was removed - handle logout for all windows
authService?.handleDeauth()
}
}
}),
)
@@ -728,8 +731,6 @@ export async function deactivate() {
// Dispose all webview instances
await WebviewProvider.disposeAllInstances()
await telemetryService.sendCollectedEvents()
// Clean up test mode
cleanupTestMode()
await posthogClientProvider.shutdown()
+82
View File
@@ -0,0 +1,82 @@
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
export class ExternalDiffViewProvider extends DiffViewProvider {
private activeDiffEditorId: string | undefined
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
return
}
const response = await getHostBridgeProvider().diffClient.openDiff({
path: this.absolutePath,
content: this.originalContent ?? "",
})
this.activeDiffEditorId = response.diffId
}
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
_currentLine: number | undefined,
): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.replaceText({
diffId: this.activeDiffEditorId,
content: content,
startLine: rangeToReplace.startLine,
endLine: rangeToReplace.endLine,
})
}
protected override async truncateDocument(lineNumber: number): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.truncateDocument({
diffId: this.activeDiffEditorId,
endLine: lineNumber,
})
}
protected async saveDocument(): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.saveDocument({ diffId: this.activeDiffEditorId })
}
protected override async scrollEditorToLine(line: number): Promise<void> {
console.log(`Called ExternalDiffViewProvider.scrollEditorToLine(${line}) stub`)
}
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
console.log(`Called ExternalDiffViewProvider.scrollAnimation(${startLine}, ${endLine}) stub`)
}
protected override async getDocumentText(): Promise<string | undefined> {
if (!this.activeDiffEditorId) {
return undefined
}
return (await getHostBridgeProvider().diffClient.getDocumentText({ diffId: this.activeDiffEditorId })).content
}
protected override async getNewDiagnosticProblems(): Promise<string> {
console.log(`Called ExternalDiffViewProvider.getNewDiagnosticProblems() stub`)
return ""
}
protected override async closeDiffView(): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.closeDiff({ diffId: this.activeDiffEditorId })
this.activeDiffEditorId = undefined
}
protected override async resetDiffView(): Promise<void> {
this.activeDiffEditorId = undefined
}
}
@@ -4,11 +4,6 @@ import * as vscode from "vscode"
import { URI } from "vscode-uri"
import { WebviewProvider } from "@core/webview"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
export class ExternalWebviewProvider extends WebviewProvider {
// This hostname cannot be changed without updating the external webview handler.
private RESOURCE_HOSTNAME: string = "internal.resources"
@@ -1,5 +1,5 @@
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
import { Controller } from "@core/controller"
/**
* Type definition for a gRPC handler function.
@@ -5,7 +5,7 @@ import {
EnvServiceClientImpl,
WindowServiceClientImpl,
DiffServiceClientImpl,
} from "@generated/standalone/host-bridge-clients"
} from "@generated/hosts/standalone/host-bridge-clients"
import {
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
@@ -14,6 +14,7 @@ import {
DiffServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
import { HOSTBRIDGE_PORT } from "../../standalone/cline-core"
/**
* Manager to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
@@ -28,7 +29,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
diffClient: DiffServiceClientInterface
constructor() {
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
const address = process.env.HOST_BRIDGE_ADDRESS || `localhost:${HOSTBRIDGE_PORT}`
this.channel = createChannel(address)
this.watchServiceClient = new WatchServiceClientImpl(this.channel)
+104 -7
View File
@@ -1,10 +1,17 @@
import { arePathsEqual } from "@/utils/path"
import * as path from "path"
import * as vscode from "vscode"
import { DecorationController } from "@integrations/editor/DecorationController"
import { DecorationController } from "@/hosts/vscode/DecorationController"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import { diagnosticsToProblemsString, getNewDiagnostics } from "@/integrations/diagnostics"
export class VscodeDiffViewProvider extends DiffViewProvider {
private activeDiffEditor?: vscode.TextEditor
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private fadedOverlayController?: DecorationController
private activeLineController?: DecorationController
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
throw new Error("No file path set")
@@ -81,20 +88,110 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number,
currentLine: number | undefined,
): Promise<void> {
const document = this.activeDiffEditor?.document
if (!document) {
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
throw new Error("User closed text editor, unable to edit file...")
}
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Replace the text in the diff editor document.
const document = this.activeDiffEditor?.document
const edit = new vscode.WorkspaceEdit()
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
edit.replace(document.uri, range, content)
await vscode.workspace.applyEdit(edit)
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
if (currentLine !== undefined) {
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
}
}
override async scrollEditorToLine(line: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const scrollLine = line + 4
this.activeDiffEditor.revealRange(new vscode.Range(scrollLine, 0, scrollLine, 0), vscode.TextEditorRevealType.InCenter)
}
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
}
override async truncateDocument(lineNumber: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const document = this.activeDiffEditor.document
if (lineNumber < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
edit.delete(document.uri, new vscode.Range(lineNumber, 0, document.lineCount, 0))
await vscode.workspace.applyEdit(edit)
}
// Clear all decorations at the end (before applying final edit)
this.fadedOverlayController?.clear()
this.activeLineController?.clear()
}
protected override async getDocumentText(): Promise<string | undefined> {
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
return undefined
}
return this.activeDiffEditor.document.getText()
}
protected override async getNewDiagnosticProblems(): Promise<string> {
// Get the diagnostics after changing the document.
const postDiagnostics = vscode.languages.getDiagnostics()
const newProblems = getNewDiagnostics(this.preDiagnostics, postDiagnostics)
// Only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
// will be empty string if no errors
const problems = await diagnosticsToProblemsString(newProblems, [vscode.DiagnosticSeverity.Error])
return problems
}
protected override async saveDocument(): Promise<void> {
if (!this.activeDiffEditor) {
return
}
if (this.activeDiffEditor.document.isDirty) {
await this.activeDiffEditor.document.save()
}
}
protected async closeDiffView(): Promise<void> {
// Close all the cline diff views.
const tabs = vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
for (const tab of tabs) {
// trying to close dirty views results in save popup
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
}
}
protected override async resetDiffView(): Promise<void> {
this.activeDiffEditor = undefined
this.fadedOverlayController = undefined
this.activeLineController = undefined
this.preDiagnostics = []
}
}
+14 -1
View File
@@ -1,5 +1,5 @@
import { StreamingCallbacks } from "@/hosts/host-provider-types"
import { HostServiceHandlerConfig, hostServiceHandlers } from "@generated/hosts/vscode/hostbridge-grpc-service-config"
import { hostServiceHandlers } from "@generated/hosts/vscode/hostbridge-grpc-service-config"
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
/**
@@ -161,6 +161,19 @@ export class GrpcHandler {
}
}
/**
* 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>
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
@@ -0,0 +1,5 @@
import { CloseDiffRequest, CloseDiffResponse } from "@/shared/proto/index.host"
export async function closeDiff(_request: CloseDiffRequest): Promise<CloseDiffResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -0,0 +1,5 @@
import { GetDocumentTextRequest, GetDocumentTextResponse } from "@/shared/proto/index.host"
export async function getDocumentText(_request: GetDocumentTextRequest): Promise<GetDocumentTextResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,5 +1,5 @@
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -0,0 +1,5 @@
import { SaveDocumentRequest, SaveDocumentResponse } from "@/shared/proto/index.host"
export async function saveDocument(_request: SaveDocumentRequest): Promise<SaveDocumentResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -0,0 +1,5 @@
import { TruncateDocumentRequest, TruncateDocumentResponse } from "@/shared/proto/index.host"
export async function truncateDocument(_request: TruncateDocumentRequest): Promise<TruncateDocumentResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,22 +1,24 @@
import { window } from "vscode"
import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/proto/index.host"
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse> {
const { message, type, options } = request
const { modal, detail, items } = options || {}
const option = items ? { modal, items } : { modal, detail }
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
const option = { modal, detail }
let selectedOption: string | undefined = undefined
switch (type) {
case ShowMessageType.ERROR:
selectedOption = await window.showErrorMessage(message, option)
selectedOption = await window.showErrorMessage(message, option, ...items)
break
case ShowMessageType.WARNING:
selectedOption = await window.showWarningMessage(message, option)
selectedOption = await window.showWarningMessage(message, option, ...items)
break
default:
selectedOption = await window.showInformationMessage(message, option)
selectedOption = await window.showInformationMessage(message, option, ...items)
break
}
+31 -2
View File
@@ -76,10 +76,39 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
)
}
} catch (err) {
// When the command fails, execa throws an error with the arguments, which include the whole system prompt.
// We want to log that, but not show it to the user.
console.error(`Error during Claude Code execution:`, err)
if (err instanceof Error) {
if (err.message.includes("ENOENT")) {
throw new Error(
`Failed to find the Claude Code executable.
Make sure it's installed and available in your PATH or properly set in your provider settings.`,
{ cause: err },
)
}
if (err.message.includes("E2BIG")) {
throw new Error(
`Executing Claude Code failed due to a long system prompt. The maximum argument length is 131072 bytes.
Rules and workflows contribute to a longer system prompt, consider disabling some of them temporarily to reduce the length.
Anthropic is aware of this issue and is considering a fix: https://github.com/anthropics/claude-code/issues/3411.
`,
{ cause: err },
)
}
if (err.message.includes("ENAMETOOLONG")) {
throw new Error(
`Executing Claude Code failed due to a long system prompt. Windows has a limit of 8191 characters, which makes the integration with Cline not work properly.
Please check our docs on how to integrate Claude Code with Cline on Windows: https://docs.cline.bot/provider-config/claude-code#windows-setup.
Anthropic is aware of this issue and is considering a fix: https://github.com/anthropics/claude-code/issues/3411.
`,
{ cause: err },
)
}
// When the command fails, execa throws an error with the arguments, which include the whole system prompt.
// We want to log that, but not show it to the user.
const startOfCommand = err.message.indexOf(": ")
if (startOfCommand !== -1) {
const messageWithoutCommand = err.message.slice(0, startOfCommand).trim()
@@ -1,75 +0,0 @@
import * as vscode from "vscode"
interface DebugSession {
id: string
name: string
output: string[]
lastRetrievedIndex: number
}
export class DebugConsoleManager {
private sessions: Map<string, DebugSession> = new Map()
private disposables: vscode.Disposable[] = []
constructor() {
// Listen for debug session start events
this.disposables.push(
vscode.debug.onDidStartDebugSession((session) => {
this.sessions.set(session.id, {
id: session.id,
name: session.name,
output: [],
lastRetrievedIndex: -1,
})
}),
)
// Listen for debug session end events
this.disposables.push(
vscode.debug.onDidTerminateDebugSession((session) => {
this.sessions.delete(session.id)
}),
)
// Listen for debug console output
this.disposables.push(
vscode.debug.onDidReceiveDebugSessionCustomEvent((e: vscode.DebugSessionCustomEvent) => {
if (e.event === "output" && e.body?.output) {
const session = this.sessions.get(e.session.id)
if (session) {
session.output.push(e.body.output)
}
}
}),
)
}
/**
* Get all active debug sessions
*/
getActiveSessions(): { id: string; name: string }[] {
return Array.from(this.sessions.values()).map(({ id, name }) => ({ id, name }))
}
/**
* Get any new output since the last retrieval for a specific debug session
*/
getUnretrievedOutput(sessionId: string): string | undefined {
const session = this.sessions.get(sessionId)
if (!session) {
return undefined
}
const newOutput = session.output.slice(session.lastRetrievedIndex + 1).join("")
session.lastRetrievedIndex = session.output.length - 1
return newOutput || undefined
}
/**
* Clean up resources
*/
dispose() {
this.disposables.forEach((d) => d.dispose())
this.sessions.clear()
}
}
@@ -1,123 +0,0 @@
/*
import * as vscode from "vscode"
import deepEqual from "fast-deep-equal"
type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]
About Diagnostics:
The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into:
Errors: Critical issues that usually prevent your code from compiling or running correctly.
Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables).
Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters).
The Problems tab displays diagnostics from various sources:
1. Language Servers:
- TypeScript: Type errors, missing imports, syntax issues
- Python: Syntax errors, invalid type hints, undefined variables
- JavaScript/Node.js: Parsing and execution errors
2. Linters:
- ESLint: Code style, best practices, potential bugs
- Pylint: Unused imports, naming conventions
- TSLint: Style and correctness issues in TypeScript
3. Build Tools:
- Webpack: Module resolution failures, build errors
- Gulp: Build errors during task execution
4. Custom Validators:
- Extensions can generate custom diagnostics for specific languages or tools
Each problem typically indicates its source (e.g., language server, linter, build tool).
Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error.
Notes on diagnostics:
- linter diagnostics are only captured for open editors
- this works great for us since when cline edits/creates files its through vscode's textedit api's and we get those diagnostics for free
- some tools might require you to save the file or manually refresh to clear the problem from the list.
System Prompt
- You will automatically receive workspace error diagnostics in environment_details. Be mindful that this may include issues beyond the scope of your task or the user's request. Only address errors relevant to your work, and avoid fixing pre-existing or unrelated issues unless the user specifically instructs you to do so.
- If you are unable to resolve errors provided in environment_details after two attempts, consider using ask_followup_question to ask the user for additional information, such as the latest documentation related to a problematic framework, to help you make progress on the task. If the error remains unresolved after this step, proceed with your task while disregarding the error.
class DiagnosticsMonitor {
private diagnosticsChangeEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>()
private disposables: vscode.Disposable[] = []
private lastDiagnostics: FileDiagnostics = []
constructor() {
this.disposables.push(
vscode.languages.onDidChangeDiagnostics(() => {
this.diagnosticsChangeEmitter.fire()
})
)
}
public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise<FileDiagnostics> {
const currentDiagnostics = this.getDiagnostics()
if (!shouldWaitForChanges) {
this.lastDiagnostics = currentDiagnostics
return currentDiagnostics
}
if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) {
this.lastDiagnostics = currentDiagnostics
return currentDiagnostics
}
let timeout = 300 // only way this happens is if there's no errors
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error)
)
if (hasErrors) {
console.log("Existing errors detected, extending timeout", currentDiagnostics)
timeout = 10_000
}
return this.waitForUpdatedDiagnostics(timeout)
}
private async waitForUpdatedDiagnostics(timeout: number): Promise<FileDiagnostics> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup()
const finalDiagnostics = this.getDiagnostics()
this.lastDiagnostics = finalDiagnostics
resolve(finalDiagnostics)
}, timeout)
const disposable = this.diagnosticsChangeEmitter.event(() => {
const updatedDiagnostics = this.getDiagnostics() // I thought this would only trigger when diagnostics changed, but that's not the case.
if (deepEqual(this.lastDiagnostics, updatedDiagnostics)) {
// diagnostics have not changed, ignoring...
return
}
cleanup()
this.lastDiagnostics = updatedDiagnostics
resolve(updatedDiagnostics)
})
const cleanup = () => {
clearTimeout(timer)
disposable.dispose()
}
})
}
private getDiagnostics(): FileDiagnostics {
const allDiagnostics = vscode.languages.getDiagnostics()
return allDiagnostics
.filter(([_, diagnostics]) => diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error))
.map(([uri, diagnostics]) => [
uri,
diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error),
])
}
public dispose() {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
this.diagnosticsChangeEmitter.dispose()
}
}
export default DiagnosticsMonitor
*/
+119 -148
View File
@@ -4,13 +4,10 @@ import * as fs from "fs/promises"
import { createDirectoriesForFile } from "@utils/fs"
import { arePathsEqual, getCwd } from "@utils/path"
import { formatResponse } from "@core/prompts/responses"
import { DecorationController } from "./DecorationController"
import * as diff from "diff"
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
import { detectEncoding } from "../misc/extract-text"
import * as iconv from "iconv-lite"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
@@ -26,11 +23,6 @@ export abstract class DiffViewProvider {
private streamedLines: string[] = []
private newContent?: string
protected activeDiffEditor?: vscode.TextEditor
protected fadedOverlayController?: DecorationController
protected activeLineController?: DecorationController
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
constructor() {}
public async open(relPath: string): Promise<void> {
@@ -62,17 +54,13 @@ export abstract class DiffViewProvider {
await fs.writeFile(this.absolutePath, "")
}
await this.openDiffEditor()
this.scrollEditorToLine(0) // will this crash for new files?
await this.scrollEditorToLine(0)
this.streamedLines = []
}
/**
* Opens a diff editor or viewer for the current file.
*
* This abstract method must be implemented by subclasses to create and display
* a diff editor or viewer that shows the difference between the original and
* modified content.
*
* Called automatically by the `open` method after ensuring the file exists and
* creating any necessary directories.
*
@@ -80,13 +68,82 @@ export abstract class DiffViewProvider {
*/
protected abstract openDiffEditor(): Promise<void>
/**
* Scrolls the diff editor to reveal a specific line.
*
* It's used during streaming updates to keep the user's view focused on the changing content.
*
* @param line The 0-based line number to scroll to
*/
protected abstract scrollEditorToLine(line: number): Promise<void>
/**
* Creates a smooth scrolling animation between two lines in the diff editor.
*
* It's typically used when updates contain many lines, to help the user visually track the flow
* of significant changes in the document.
*
* @param startLine The 0-based line number to begin the animation from
* @param endLine The 0-based line number to animate to
*/
protected abstract scrollAnimation(startLine: number, endLine: number): Promise<void>
/**
* Removes content from the specified line to the end of the document.
* Called after the final update is received.
*/
protected abstract truncateDocument(lineNumber: number): Promise<void>
/**
* Get the contents of the diff editor document.
*
* Returns undefined if the diff editor was closed.
*/
protected abstract getDocumentText(): Promise<string | undefined>
/**
* Get any new diagnostic problems that appeared after applying the diff.
*
* Getting diagnostics before and after the file edit is a better approach than
* automatically tracking problems in real-time. This method ensures we only
* report new problems that are a direct result of this specific edit.
* Since these are new problems resulting from Cline's edit, we know they're
* directly related to the work he's doing. This eliminates the risk of Cline
* going off-task or getting distracted by unrelated issues, which was a problem
* with the previous auto-debug approach. Some users' machines may be slow to
* update diagnostics, so this approach provides a good balance between automation
* and avoiding potential issues where Cline might get stuck in loops due to
* outdated problem information. If no new problems show up by the time the user
* accepts the changes, they can always debug later using the '@problems' mention.
* This way, Cline only becomes aware of new problems resulting from his edits
* and can address them accordingly. If problems don't change immediately after
* applying a fix, Cline won't be notified, which is generally fine since the
* initial fix is usually correct and it may just take time for linters to catch up.
*/
protected abstract getNewDiagnosticProblems(): Promise<string>
/**
* Save the contents of the diff editor UI to the file.
*/
protected abstract saveDocument(): Promise<void>
/**
* Closes the diff editor tab or window.
*/
protected abstract closeDiffView(): Promise<void>
/**
* Cleans up the diff view resources and resets internal state.
*/
protected abstract resetDiffView(): Promise<void>
async update(
accumulatedContent: string,
isFinal: boolean,
changeLocation?: { startLine: number; endLine: number; startChar: number; endChar: number },
) {
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
throw new Error("Required values not set")
if (!this.isEditing) {
throw new Error("Not editing any file")
}
// --- Fix to prevent duplicate BOM ---
@@ -104,16 +161,6 @@ export abstract class DiffViewProvider {
}
const diffLines = accumulatedLines.slice(this.streamedLines.length)
const diffEditor = this.activeDiffEditor
const document = diffEditor?.document
if (!diffEditor || !document) {
throw new Error("User closed text editor, unable to edit file...")
}
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Instead of animating each line, we'll update in larger chunks
const currentLine = this.streamedLines.length + diffLines.length - 1
if (currentLine >= 0) {
@@ -129,30 +176,19 @@ export abstract class DiffViewProvider {
if (changeLocation) {
// We have the actual location of the change, scroll to it
const targetLine = changeLocation.startLine
this.scrollEditorToLine(targetLine)
await this.scrollEditorToLine(targetLine)
} else {
// Fallback to the old logic for non-replacement updates
if (diffLines.length <= 5) {
// For small changes, just jump directly to the line
this.scrollEditorToLine(currentLine)
await this.scrollEditorToLine(currentLine)
} else {
// For larger changes, create a quick scrolling animation
const startLine = this.streamedLines.length
const endLine = currentLine
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor?.revealRange(
new vscode.Range(line, 0, line, 0),
vscode.TextEditorRevealType.InCenter,
)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
await this.scrollAnimation(startLine, endLine)
// Ensure we end at the final line
this.scrollEditorToLine(currentLine)
await this.scrollEditorToLine(currentLine)
}
}
}
@@ -161,11 +197,8 @@ export abstract class DiffViewProvider {
this.streamedLines = accumulatedLines
if (isFinal) {
// Handle any remaining lines if the new content is shorter than the original
if (this.streamedLines.length < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
await vscode.workspace.applyEdit(edit)
}
await this.truncateDocument(this.streamedLines.length)
// Add empty last line if original content had one
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
if (hasEmptyLastLine) {
@@ -174,9 +207,6 @@ export abstract class DiffViewProvider {
accumulatedContent += "\n"
}
}
// Clear all decorations at the end (before applying final edit)
this.fadedOverlayController.clear()
this.activeLineController.clear()
}
}
@@ -195,7 +225,7 @@ export abstract class DiffViewProvider {
abstract replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number,
currentLine: number | undefined,
): Promise<void>
async saveChanges(): Promise<{
@@ -204,7 +234,10 @@ export abstract class DiffViewProvider {
autoFormattingEdits: string | undefined
finalContent: string | undefined
}> {
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
// get the contents before save operation which may do auto-formatting
const preSaveContent = await this.getDocumentText()
if (!this.relPath || !this.absolutePath || !this.newContent || preSaveContent === undefined) {
return {
newProblemsMessage: undefined,
userEdits: undefined,
@@ -212,50 +245,21 @@ export abstract class DiffViewProvider {
finalContent: undefined,
}
}
const updatedDocument = this.activeDiffEditor.document
// get the contents before save operation which may do auto-formatting
const preSaveContent = updatedDocument.getText()
if (updatedDocument.isDirty) {
await updatedDocument.save()
}
await this.saveDocument()
// get text after save in case there is any auto-formatting done by the editor
const postSaveContent = updatedDocument.getText()
const postSaveContent = (await this.getDocumentText()) || ""
await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: this.absolutePath,
options: ShowTextDocumentOptions.create({
preview: false,
preserveFocus: true,
}),
}),
)
await this.closeAllDiffViews()
await getHostBridgeProvider().windowClient.showTextDocument({
path: this.absolutePath,
options: {
preview: false,
preserveFocus: true,
},
})
await this.closeDiffView()
/*
Getting diagnostics before and after the file edit is a better approach than
automatically tracking problems in real-time. This method ensures we only
report new problems that are a direct result of this specific edit.
Since these are new problems resulting from Cline's edit, we know they're
directly related to the work he's doing. This eliminates the risk of Cline
going off-task or getting distracted by unrelated issues, which was a problem
with the previous auto-debug approach. Some users' machines may be slow to
update diagnostics, so this approach provides a good balance between automation
and avoiding potential issues where Cline might get stuck in loops due to
outdated problem information. If no new problems show up by the time the user
accepts the changes, they can always debug later using the '@problems' mention.
This way, Cline only becomes aware of new problems resulting from his edits
and can address them accordingly. If problems don't change immediately after
applying a fix, Cline won't be notified, which is generally fine since the
initial fix is usually correct and it may just take time for linters to catch up.
*/
const postDiagnostics = vscode.languages.getDiagnostics()
const newProblems = await diagnosticsToProblemsString(getNewDiagnostics(this.preDiagnostics, postDiagnostics), [
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
]) // will be empty string if no errors
const newProblems = await this.getNewDiagnosticProblems()
const newProblemsMessage =
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
@@ -295,16 +299,14 @@ export abstract class DiffViewProvider {
}
async revertChanges(): Promise<void> {
if (!this.absolutePath || !this.activeDiffEditor) {
if (!this.absolutePath) {
return
}
const fileExists = this.editType === "modify"
const updatedDocument = this.activeDiffEditor.document
if (!fileExists) {
if (updatedDocument.isDirty) {
await updatedDocument.save()
}
await this.closeAllDiffViews()
await this.saveDocument()
await this.closeDiffView()
await fs.unlink(this.absolutePath)
// Remove only the directories we created, in reverse order
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
@@ -314,70 +316,41 @@ export abstract class DiffViewProvider {
console.log(`File ${this.absolutePath} has been deleted.`)
} else {
// revert document
const edit = new vscode.WorkspaceEdit()
const fullRange = new vscode.Range(
updatedDocument.positionAt(0),
updatedDocument.positionAt(updatedDocument.getText().length),
)
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of
// course the user made changes and saved during the edit.
const contents = (await this.getDocumentText()) || ""
const lineCount = (contents.match(/\n/g) || []).length + 1
await this.replaceText(this.originalContent ?? "", { startLine: 0, endLine: lineCount }, undefined)
await this.saveDocument()
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
if (this.documentWasOpen) {
await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: this.absolutePath,
options: ShowTextDocumentOptions.create({
preview: false,
preserveFocus: true,
}),
}),
)
await getHostBridgeProvider().windowClient.showTextDocument({
path: this.absolutePath,
options: {
preview: false,
preserveFocus: true,
},
})
}
await this.closeAllDiffViews()
await this.closeDiffView()
}
// edit is done
await this.reset()
}
private async closeAllDiffViews() {
const tabs = vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
for (const tab of tabs) {
// trying to close dirty views results in save popup
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
}
}
private scrollEditorToLine(line: number) {
if (this.activeDiffEditor) {
const scrollLine = line + 4
this.activeDiffEditor.revealRange(
new vscode.Range(scrollLine, 0, scrollLine, 0),
vscode.TextEditorRevealType.InCenter,
)
}
}
scrollToFirstDiff() {
if (!this.activeDiffEditor) {
async scrollToFirstDiff() {
if (!this.isEditing) {
return
}
const currentContent = this.activeDiffEditor.document.getText()
const currentContent = (await this.getDocumentText()) || ""
const diffs = diff.diffLines(this.originalContent || "", currentContent)
let lineCount = 0
for (const part of diffs) {
if (part.added || part.removed) {
// Found the first diff, scroll to it
this.activeDiffEditor.revealRange(
new vscode.Range(lineCount, 0, lineCount, 0),
vscode.TextEditorRevealType.InCenter,
)
this.scrollEditorToLine(lineCount)
return
}
if (!part.removed) {
@@ -393,10 +366,8 @@ export abstract class DiffViewProvider {
this.originalContent = undefined
this.createdDirs = []
this.documentWasOpen = false
this.activeDiffEditor = undefined
this.fadedOverlayController = undefined
this.activeLineController = undefined
this.streamedLines = []
this.preDiagnostics = []
await this.resetDiffView()
}
}
@@ -59,12 +59,10 @@ export function extractCommitMessage(aiResponse: string): string {
*/
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
await writeTextToClipboard(message)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message copied to clipboard",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message copied to clipboard",
})
}
/**
@@ -77,18 +75,16 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
const editAction = "Edit Message"
const selectedAction = (
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message generated",
options: {
modal: false,
detail: message,
items: [copyAction, applyAction, editAction],
},
}),
)
)?.selectedOption
await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message generated",
options: {
modal: false,
detail: message,
items: [copyAction, applyAction, editAction],
},
})
).selectedOption
// Handle user dismissing the dialog (selectedAction is undefined)
if (!selectedAction) {
@@ -120,28 +116,22 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = message
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message applied to Git input",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message applied to Git input",
})
} else {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No Git repositories found",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No Git repositories found",
})
await copyCommitMessageToClipboard(message)
}
} else {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Git extension not found",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Git extension not found",
})
await copyCommitMessageToClipboard(message)
}
}
@@ -161,10 +151,8 @@ async function editCommitMessage(message: string): Promise<void> {
path: document.uri.fsPath,
}),
)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Edit the commit message and copy when ready",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Edit the commit message and copy when ready",
})
}
+4 -6
View File
@@ -48,12 +48,10 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
}),
)
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
})
}
}
}
+17 -26
View File
@@ -3,18 +3,16 @@ import * as os from "os"
import * as vscode from "vscode"
import { arePathsEqual } from "@utils/path"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { writeFile } from "@utils/fs"
export async function openImage(dataUri: string) {
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
if (!matches) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid data URI format",
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Invalid data URI format",
})
return
}
const [, format, base64Data] = matches
@@ -24,12 +22,10 @@ export async function openImage(dataUri: string) {
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error opening image: ${error}`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error opening image: ${error}`,
})
}
}
@@ -54,19 +50,14 @@ export async function openFile(absolutePath: string) {
}
} catch {} // not essential, sometimes tab operations fail
const document = await vscode.workspace.openTextDocument(uri)
await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: document.uri.fsPath,
options: ShowTextDocumentOptions.create({ preview: false }),
}),
)
await getHostBridgeProvider().windowClient.showTextDocument({
path: uri.fsPath,
options: { preview: false },
})
} catch (error) {
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not open file!`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Could not open file!`,
})
}
}
+16 -24
View File
@@ -46,22 +46,18 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
})
return null
}
} catch (error) {
console.error(`Error reading file or getting dimensions for ${filePath}:`, error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
})
return null
}
@@ -76,22 +72,18 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const stats = await fs.stat(filePath)
if (stats.size > 20 * 1000 * 1024) {
console.warn(`File too large, skipping: ${filePath}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
})
return null
}
} catch (error) {
console.error(`Error checking file size for ${filePath}:`, error)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
})
return null
}
return { type: "file", data: filePath }
+370 -274
View File
@@ -31,20 +31,35 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// constructor() {
// super()
async run(terminal: vscode.Terminal, command: string) {
// When command does not produce any output, we can assume the shell integration API failed and as a fallback return the current terminal contents
const emitCurrentTerminalContents = async () => {
try {
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
const fallbackMessage = `The command's output could not be captured due to some technical issue, however it has been executed successfully. Here's the current terminal's content to help you get the command's output:\n\n${terminalSnapshot}`
this.emit("line", fallbackMessage)
}
} catch (error) {
console.error("Error capturing terminal output:", error)
private async emitCurrentTerminalContents(): Promise<void> {
try {
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
const fallbackMessage = `The command's output could not be captured due to some technical issue, however it has been executed successfully. Here's the current terminal's content to help you get the command's output:\n\n${terminalSnapshot}`
this.emit("line", fallbackMessage)
}
} catch (error) {
console.error("Error capturing terminal output:", error)
}
}
async run(terminal: vscode.Terminal, command: string) {
// Initialize state for new command
await this.initializeForNewCommand(command)
console.log(`[TerminalProcess] Starting command: "${command}"`)
console.log(`[TerminalProcess] Shell integration available: ${!!terminal.shellIntegration?.executeCommand}`)
console.log(`[TerminalProcess] Terminal ID: ${terminal.name}`)
if (terminal.shellIntegration?.executeCommand) {
await this.runWithShellIntegration(terminal, command)
} else {
// no shell integration detected, we'll fallback to running the command and capturing the terminal's output after some time
await this.runWithoutShellIntegration(terminal, command)
}
}
private async initializeForNewCommand(command: string): Promise<void> {
// Clear any existing grace period timer from previous commands
if (this.gracePeriodTimer) {
clearTimeout(this.gracePeriodTimer)
@@ -65,291 +80,372 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
this.lastRetrievedIndex = 0
this.isListening = true
this.isHot = false
this.command = command
console.log(`[TerminalProcess] Starting command: "${command}"`)
console.log(`[TerminalProcess] Shell integration available: ${!!terminal.shellIntegration?.executeCommand}`)
console.log(`[TerminalProcess] Terminal ID: ${terminal.name}`)
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
let execution
let stream
}
try {
execution = terminal.shellIntegration.executeCommand(command)
stream = execution.read()
} catch (error) {
console.error(`[TerminalProcess] Failed to execute command: ${error}`)
this.emit("error", error as Error)
return
private async runWithShellIntegration(terminal: vscode.Terminal, command: string): Promise<void> {
// Execute command and get stream
const stream = await this.executeCommandWithShellIntegration(terminal, command)
if (!stream) {
return
}
// Initialize state for stream processing
const streamState = this.initializeStreamState()
// Set up timeout for commands with no output
const firstChunkTimeout = this.setupNoOutputTimeout(command, streamState)
// Process the output stream
await this.processOutputStream(stream, command, streamState, firstChunkTimeout)
// Handle post-stream cleanup
this.cleanupAfterStream(streamState, firstChunkTimeout)
// Determine command completion behavior
await this.handleCommandCompletion(command, streamState)
}
private async executeCommandWithShellIntegration(terminal: vscode.Terminal, command: string): Promise<any> {
try {
if (!terminal.shellIntegration || !terminal.shellIntegration.executeCommand) {
throw new Error("Shell integration not available")
}
const execution = terminal.shellIntegration.executeCommand(command)
return execution.read()
} catch (error) {
console.error(`[TerminalProcess] Failed to execute command: ${error}`)
this.emit("error", error as Error)
return null
}
}
// todo: need to handle errors
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let receivedFirstChunk = false
private initializeStreamState() {
return {
isFirstChunk: true,
didOutputNonCommand: false,
didEmitEmptyLine: false,
receivedFirstChunk: false,
}
}
// Set up a timeout to emit empty line if no output is received within 3 seconds
// This ensures the "proceed while running" button appears even for commands with no/delayed output
const firstChunkTimeout = setTimeout(() => {
if (!receivedFirstChunk && !didEmitEmptyLine) {
console.log(`[TerminalProcess] First chunk timeout fired - no output received within 3s for: "${command}"`)
this.emit("line", "") // empty line to show proceed button
didEmitEmptyLine = true
private setupNoOutputTimeout(command: string, streamState: any): NodeJS.Timeout {
// Set up a 3-second timeout to handle commands with no/delayed output.
// This ensures the UI remains responsive by:
// 1. Showing the "proceed while running" button after 3 seconds
// 2. Capturing current terminal contents in case shell integration missed output
// 3. Informing the user that the command is still running
return setTimeout(async () => {
if (!streamState.receivedFirstChunk && !streamState.didEmitEmptyLine) {
console.log(`[TerminalProcess] First chunk timeout fired - no output received within 3s for: "${command}"`)
this.emit("line", "") // empty line to show proceed button
streamState.didEmitEmptyLine = true
// Also emit a message indicating the command might be running without output
this.emit("line", "[Command is running but producing no output]")
}
}, 3000) // 3 second timeout
for await (let data of stream) {
// Clear the timeout since we received output
if (!receivedFirstChunk) {
clearTimeout(firstChunkTimeout)
receivedFirstChunk = true
console.log(`[TerminalProcess] First chunk received for command: "${command}"`)
}
// Log raw data length
console.log(`[TerminalProcess] Raw data chunk received: ${data.length} chars`)
if (!data || data.trim() === "") {
console.log(`[TerminalProcess] WARNING: Received empty or whitespace-only chunk`)
}
// 1. Process chunk and remove artifacts
if (isFirstChunk) {
/*
The first chunk we get from this stream needs to be processed to be more human readable, ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc.
*/
// bug where sometimes the command output makes its way into vscode shell integration metadata
/*
]633 is a custom sequence number used by VSCode shell integration:
- OSC 633 ; A ST - Mark prompt start
- OSC 633 ; B ST - Mark prompt end
- OSC 633 ; C ST - Mark pre-execution (start of command output)
- OSC 633 ; D [; <exitcode>] ST - Mark execution finished with optional exit code
- OSC 633 ; E ; <commandline> [; <nonce>] ST - Explicitly set command line with optional nonce
*/
// if you print this data you might see something like "eecho hello worldo hello world;5ba85d14-e92a-40c4-b2fd-71525581eeb0]633;C" but this is actually just a bunch of escape sequences, ignore up to the first ;C
/* ddateb15026-6a64-40db-b21f-2a621a9830f0]633;CTue Sep 17 06:37:04 EDT 2024 % ]633;D;0]633;P;Cwd=/Users/saoud/Repositories/test */
// Gets output between ]633;C (command start) and ]633;D (command end)
const outputBetweenSequences = this.removeLastLineArtifacts(
data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "",
).trim()
// Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
if (lastMatch && lastMatch.index !== undefined) {
data = data.slice(lastMatch.index + lastMatch[0].length)
}
// Place output back after removing vscode sequences
if (outputBetweenSequences) {
data = outputBetweenSequences + "\n" + data
}
// remove ansi
data = stripAnsi(data)
// Split data by newlines
const lines = data ? data.split("\n") : []
// Remove non-human readable characters from the first line
if (lines.length > 0) {
lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
}
// Check for duplicated first character that might be a terminal artifact
// But skip this check for known syntax characters like {, [, ", etc.
if (
lines.length > 0 &&
lines[0].length >= 2 &&
lines[0][0] === lines[0][1] &&
!["[", "{", '"', "'", "<", "("].includes(lines[0][0])
) {
lines[0] = lines[0].slice(1)
}
// Only remove specific terminal artifacts from line beginnings while preserving JSON syntax
if (lines.length > 0) {
// This regex only removes common terminal artifacts (%, $, >, #) and invisible control chars
// but preserves important syntax chars like {, [, ", etc.
lines[0] = lines[0].replace(/^[\x00-\x1F%$>#\s]*/, "")
}
if (lines.length > 1) {
lines[1] = lines[1].replace(/^[\x00-\x1F%$>#\s]*/, "")
}
// Join lines back
data = lines.join("\n")
isFirstChunk = false
} else {
data = stripAnsi(data)
}
// Ctrl+C detection: if user presses Ctrl+C, treat as command terminated
if (data.includes("^C") || data.includes("\u0003")) {
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
this.isHot = false
break
}
// first few chunks could be the command being echoed back, so we must ignore
// note this means that 'echo' commands won't work
if (!didOutputNonCommand) {
const lines = data.split("\n")
for (let i = 0; i < lines.length; i++) {
if (command.includes(lines[i].trim())) {
lines.splice(i, 1)
i-- // Adjust index after removal
} else {
didOutputNonCommand = true
break
}
}
data = lines.join("\n")
}
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
// these markers indicate the command is some kind of local dev server recompiling the app, which we want to wait for output of before sending request to cline
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
const isCompiling =
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
this.hotTimer = setTimeout(
() => {
this.isHot = false
},
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
)
// For non-immediately returning commands we want to show loading spinner right away but this wouldn't happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
if (!didEmitEmptyLine && !this.fullOutput && data) {
this.emit("line", "") // empty line to indicate start of command output stream
didEmitEmptyLine = true
}
this.fullOutput += data
if (this.isListening) {
this.emitIfEol(data)
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
}
// Capture terminal contents as fallback for shell integration issues
await this.emitCurrentTerminalContents()
}
}, 3000)
}
this.emitRemainingBufferIfListening()
// Clean up the first chunk timeout if it's still active
if (!receivedFirstChunk) {
private async processOutputStream(
stream: any,
command: string,
streamState: any,
firstChunkTimeout: NodeJS.Timeout,
): Promise<void> {
for await (let data of stream) {
// Handle first chunk received
if (!streamState.receivedFirstChunk) {
clearTimeout(firstChunkTimeout)
console.log(`[TerminalProcess] WARNING: Stream ended without receiving any chunks for command: "${command}"`)
// If we never received any chunks and haven't emitted anything yet, emit now
if (!didEmitEmptyLine) {
console.log(`[TerminalProcess] Emitting fallback empty line for no-output command`)
this.emit("line", "") // empty line to show proceed button
// this.emit("line", "[Command completed with no output]")
await emitCurrentTerminalContents()
didEmitEmptyLine = true
}
streamState.receivedFirstChunk = true
console.log(`[TerminalProcess] First chunk received for command: "${command}"`)
}
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
this.isHot = false
console.log(`[TerminalProcess] Stream ended for command: "${command}"`)
console.log(`[TerminalProcess] Final output length: ${this.fullOutput.length} characters`)
// Check if this looks like a command that completed vs one that's still running
const quickCommands = ["cd ", "pwd", "ls ", "echo ", "mkdir ", "touch ", "rm ", "cp ", "mv "]
const isQuickCommand = quickCommands.some((cmd) => command.startsWith(cmd) || command.includes(" && " + cmd))
// Check if output suggests a long-running process
const longRunningIndicators = [
"listening on",
"server running",
"started on",
"watching for",
"compiled successfully",
"webpack",
"vite",
"nodemon",
"dev server",
"press ctrl",
"to quit",
"to exit",
"to stop",
]
const hasLongRunningOutput = longRunningIndicators.some((indicator) =>
this.fullOutput.toLowerCase().includes(indicator),
)
// Check if this is likely a command that starts a server or long-running process
const longRunningCommands = ["npm run", "npm start", "yarn", "node ", "python ", "serve", "dev", "watch"]
const isLongRunningCommand = longRunningCommands.some((cmd) => command.includes(cmd))
if (this.fullOutput.length === 0) {
console.log(`[TerminalProcess] WARNING: Process completed but no output was captured`)
// Ensure we emit at least one line for UI feedback
if (!didEmitEmptyLine) {
// this.emit("line", "[Command completed silently]")
await emitCurrentTerminalContents()
}
// Log chunk info
console.log(`[TerminalProcess] Raw data chunk received: ${data.length} chars`)
if (!data || data.trim() === "") {
console.log(`[TerminalProcess] WARNING: Received empty or whitespace-only chunk`)
}
// Only skip grace period for truly quick commands that have no output or are known to complete instantly
if ((this.fullOutput.length === 0 || isQuickCommand) && !isLongRunningCommand && !hasLongRunningOutput) {
console.log(`[TerminalProcess] Command appears to have completed immediately, skipping grace period`)
this.emit("completed")
this.emit("continue")
} else {
console.log(
`[TerminalProcess] Command may still be running (longRunningCommand: ${isLongRunningCommand}, longRunningOutput: ${hasLongRunningOutput})`,
)
console.log(`[TerminalProcess] Starting grace period to detect true completion...`)
// Start grace period - wait 2.5 seconds to see if more output comes
// This prevents premature "proceed while running" for commands that clearly finished
this.startGracePeriod()
// Process the chunk
data = await this.processChunk(data, command, streamState)
// Check for Ctrl+C interruption
if (this.isCommandInterrupted(data)) {
this.handleInterruption()
break
}
// Update hot/cool state based on output
this.updateHotState(data)
// Handle output emission
this.handleChunkOutput(data, streamState)
}
// Emit any remaining buffer content
this.emitRemainingBufferIfListening()
}
private async processChunk(data: string, command: string, streamState: any): Promise<string> {
if (streamState.isFirstChunk) {
data = this.cleanFirstChunk(data)
streamState.isFirstChunk = false
} else {
// no shell integration detected, we'll fallback to running the command and capturing the terminal's output after some time
terminal.sendText(command, true)
data = stripAnsi(data)
}
// wait 3 seconds for the command to run
await new Promise((resolve) => setTimeout(resolve, 3000))
// Remove command echo if not yet seen real output
if (!streamState.didOutputNonCommand) {
data = this.removeCommandEcho(data, command, streamState)
}
// For terminals without shell integration, also try to capture terminal content
await emitCurrentTerminalContents()
// For terminals without shell integration, we can't know when the command completes
// So we'll just emit the continue event after a delay
return data
}
private cleanFirstChunk(data: string): string {
/*
The first chunk we get from this stream needs to be processed to be more human readable,
ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc.
*/
// bug where sometimes the command output makes its way into vscode shell integration metadata
/*
]633 is a custom sequence number used by VSCode shell integration:
- OSC 633 ; A ST - Mark prompt start
- OSC 633 ; B ST - Mark prompt end
- OSC 633 ; C ST - Mark pre-execution (start of command output)
- OSC 633 ; D [; <exitcode>] ST - Mark execution finished with optional exit code
- OSC 633 ; E ; <commandline> [; <nonce>] ST - Explicitly set command line with optional nonce
*/
// Gets output between ]633;C (command start) and ]633;D (command end)
const outputBetweenSequences = this.removeLastLineArtifacts(data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "").trim()
// Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
if (lastMatch && lastMatch.index !== undefined) {
data = data.slice(lastMatch.index + lastMatch[0].length)
}
// Place output back after removing vscode sequences
if (outputBetweenSequences) {
data = outputBetweenSequences + "\n" + data
}
// remove ansi codes
data = stripAnsi(data)
// Split data by newlines for line-by-line processing
const lines = data ? data.split("\n") : []
// Remove non-human readable characters from the first line
if (lines.length > 0) {
lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
}
// Check for duplicated first character that might be a terminal artifact
// But skip this check for known syntax characters like {, [, ", etc.
if (
lines.length > 0 &&
lines[0].length >= 2 &&
lines[0][0] === lines[0][1] &&
!["[", "{", '"', "'", "<", "("].includes(lines[0][0])
) {
lines[0] = lines[0].slice(1)
}
// Remove specific terminal artifacts from line beginnings while preserving JSON syntax
if (lines.length > 0) {
// This regex only removes common terminal artifacts (%, $, >, #) and invisible control chars
// but preserves important syntax chars like {, [, ", etc.
lines[0] = lines[0].replace(/^[\x00-\x1F%$>#\s]*/, "")
}
if (lines.length > 1) {
lines[1] = lines[1].replace(/^[\x00-\x1F%$>#\s]*/, "")
}
// Join lines back
return lines.join("\n")
}
private removeCommandEcho(data: string, command: string, streamState: any): string {
// first few chunks could be the command being echoed back, so we must ignore
// note this means that 'echo' commands won't work properly
const lines = data.split("\n")
for (let i = 0; i < lines.length; i++) {
const trimmedLine = lines[i].trim()
// Check if the line is the command being echoed back
if (trimmedLine && trimmedLine === command.trim()) {
lines.splice(i, 1)
i-- // Adjust index after removal
} else if (trimmedLine) {
// We've hit actual output, not just the command echo
streamState.didOutputNonCommand = true
break
}
}
return lines.join("\n")
}
private isCommandInterrupted(data: string): boolean {
// Ctrl+C detection: if user presses Ctrl+C, treat as command terminated
return data.includes("^C") || data.includes("\u0003")
}
private handleInterruption(): void {
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
this.isHot = false
}
private updateHotState(data: string): void {
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
const isCompiling = this.detectCompilationStatus(data)
const timeout = isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL
this.hotTimer = setTimeout(() => {
this.isHot = false
}, timeout)
}
private detectCompilationStatus(data: string): boolean {
// these markers indicate the command is some kind of local dev server recompiling the app
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
return (
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
)
}
private handleChunkOutput(data: string, streamState: any): void {
// For non-immediately returning commands we want to show loading spinner right away
if (!streamState.didEmitEmptyLine && !this.fullOutput && data) {
this.emit("line", "") // empty line to indicate start of command output stream
streamState.didEmitEmptyLine = true
}
this.fullOutput += data
if (this.isListening) {
this.emitIfEol(data)
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
}
}
private cleanupAfterStream(streamState: any, firstChunkTimeout: NodeJS.Timeout): void {
// Clean up the first chunk timeout if it's still active
if (!streamState.receivedFirstChunk) {
clearTimeout(firstChunkTimeout)
console.log(`[TerminalProcess] WARNING: Stream ended without receiving any chunks for command: "${this.command}"`)
}
// Clear hot timer
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
this.isHot = false
console.log(`[TerminalProcess] Stream ended for command: "${this.command}"`)
console.log(`[TerminalProcess] Final output length: ${this.fullOutput.length} characters`)
}
private async handleCommandCompletion(command: string, streamState: any): Promise<void> {
const commandType = this.analyzeCommandType(command)
// Handle commands with no output
if (this.fullOutput.length === 0) {
console.log(`[TerminalProcess] WARNING: Process completed but no output was captured`)
if (!streamState.didEmitEmptyLine) {
await this.emitCurrentTerminalContents()
}
}
// Determine if we should skip grace period
if (commandType.isQuickCommand && !commandType.isLongRunning) {
console.log(`[TerminalProcess] Command appears to have completed immediately, skipping grace period`)
this.emit("completed")
this.emit("continue")
this.emit("no_shell_integration")
// setTimeout(() => {
// console.log(`Emitting continue after delay for terminal`)
// // can't emit completed since we don't if the command actually completed, it could still be running server
// }, 500) // Adjust this delay as needed
} else {
console.log(`[TerminalProcess] Command may still be running (longRunningCommand: ${commandType.isLongRunning})`)
console.log(`[TerminalProcess] Starting grace period to detect true completion...`)
this.startGracePeriod()
}
}
private analyzeCommandType(command: string): { isQuickCommand: boolean; isLongRunning: boolean } {
// Check if this looks like a command that completed vs one that's still running
const quickCommands = ["cd ", "pwd", "ls ", "echo ", "mkdir ", "touch ", "rm ", "cp ", "mv "]
const isQuickCommand = quickCommands.some((cmd) => command.startsWith(cmd) || command.includes(" && " + cmd))
// Check if output suggests a long-running process
const longRunningIndicators = [
"listening on",
"server running",
"started on",
"watching for",
"compiled successfully",
"webpack",
"vite",
"nodemon",
"dev server",
"press ctrl",
"to quit",
"to exit",
"to stop",
]
const hasLongRunningOutput = longRunningIndicators.some((indicator) => this.fullOutput.toLowerCase().includes(indicator))
// Check if this is likely a command that starts a server or long-running process
const longRunningCommands = ["npm run", "npm start", "yarn", "node ", "python ", "serve", "dev", "watch"]
const isLongRunningCommand = longRunningCommands.some((cmd) => command.includes(cmd))
return {
isQuickCommand: this.fullOutput.length === 0 || isQuickCommand,
isLongRunning: isLongRunningCommand || hasLongRunningOutput,
}
}
private async runWithoutShellIntegration(terminal: vscode.Terminal, command: string): Promise<void> {
// Send command to terminal
terminal.sendText(command, true)
// wait 3 seconds for the command to run
await new Promise((resolve) => setTimeout(resolve, 3000))
// For terminals without shell integration, also try to capture terminal content
await this.emitCurrentTerminalContents()
// For terminals without shell integration, we can't know when the command completes
// So we'll just emit the continue event after a delay
this.emit("completed")
this.emit("continue")
this.emit("no_shell_integration")
// setTimeout(() => {
// console.log(`Emitting continue after delay for terminal`)
// // can't emit completed since we don't if the command actually completed, it could still be running server
// }, 500) // Adjust this delay as needed
}
// Inspired by https://github.com/sindresorhus/execa/blob/main/lib/transform/split.js
private emitIfEol(chunk: string) {
this.buffer += chunk
+2 -4
View File
@@ -8,14 +8,12 @@ import type {
UserResponse,
} from "@shared/ClineAccount"
import { AuthService } from "../auth/AuthService"
import { clineEnvConfig } from "@/config"
export class ClineAccountService {
private static instance: ClineAccountService
private _authService: AuthService
// TODO: replace this with a global API Host
private readonly _baseUrl = "https://api.cline.bot"
// private readonly _baseUrl = "https://core-api.staging.int.cline.bot"
// private readonly _baseUrl = "http://localhost:7777"
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
constructor() {
this._authService = AuthService.getInstance()
+3 -34
View File
@@ -5,10 +5,9 @@ import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
import { Controller } from "@/core/controller"
import { storeSecret } from "@/core/storage/state"
import { clineEnvConfig } from "@/config"
const DefaultClineAccountURI = "https://app.cline.bot/auth"
// const DefaultClineAccountURI = "https://staging-app.cline.bot/auth"
// const DefaultClineAccountURI = "http://localhost:3000/auth"
const DefaultClineAccountURI = `${clineEnvConfig.appBaseUrl}/auth`
let authProviders: any[] = []
type ServiceConfig = {
@@ -70,37 +69,7 @@ export class AuthService {
const authProvidersConfigs = [
{
name: "firebase",
config: {
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
authDomain: "cline-prod.firebaseapp.com",
projectId: "cline-prod",
storageBucket: "cline-prod.firebasestorage.app",
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
// Uncomment for staging environment
// config: {
// apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
// authDomain: "cline-staging.firebaseapp.com",
// projectId: "cline-staging",
// storageBucket: "cline-staging.firebasestorage.app",
// messagingSenderId: "853479478430",
// appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
// },
// Uncomment for local development environment
// config: {
// apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
// authDomain: "cline-staging.firebaseapp.com",
// projectId: "cline-staging",
// storageBucket: "cline-staging.firebasestorage.app",
// messagingSenderId: "853479478430",
// appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
// },
// config: {
// apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
// authDomain: "cline-preview.firebaseapp.com",
// projectId: "cline-preview",
// }
config: clineEnvConfig.firebase,
},
]
@@ -6,6 +6,7 @@ import { GithubAuthProvider, GoogleAuthProvider, User, getAuth, signInWithCreden
import { ExtensionContext } from "vscode"
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
import { jwtDecode } from "jwt-decode"
import { clineEnvConfig } from "@/config"
export class FirebaseAuthProvider {
private _config: any
@@ -69,7 +70,7 @@ export class FirebaseAuthProvider {
// Now retrieve the user info from the backend (this was an easy solution to keep providing user profile details like name and email, but we should move to using the fetchMe() function instead)
// Fetch user info from Cline API
// TODO: consolidate with fetchMe() instead of making the call directly here
const userResponse = await axios.get("https://api.cline.bot/api/v1/users/me", {
const userResponse = await axios.get(`${clineEnvConfig.apiBaseUrl}/api/v1/users/me`, {
headers: {
Authorization: `Bearer ${idToken}`,
},

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