Compare commits

..

212 Commits

Author SHA1 Message Date
Saoud Rizwan 8a2679fc30 Revert "Add IS_DEV and Hot Reloading to debug. (#1895)"
This reverts commit b0446f7bac.
2025-02-23 16:10:14 -08:00
Dennis Bartlett b0446f7bac Add IS_DEV and Hot Reloading to debug. (#1895)
* Add IS_DEV and Hot Reloading to debug.

* Changeset Added

* Clean logs
2025-02-21 00:21:00 -08:00
Deepak Mangla f8e4bfdbeb Fix 'Contributing to cline' URL in docs. (#1891) 2025-02-21 00:05:33 -08:00
pashpashpash 9b30efefbe Merge pull request #1874 from Quentin-M/fix/aws_bedrock_credentials
fix: make AWS Bedrock authentication predictable
2025-02-20 16:34:19 -08:00
Quentin Machu 82f1f79b3e fix: make AWS Bedrock authentication predictable
Anthropic's Bedrock SDK creates an AWS credential provider chain [1]
for each request it needs to sign. By doing so right before actually
having to sign the request, it can utilize any session created after
VSCode launched (i.e. outside of the process), and it can renew the
sessions every time necessary transparently for the user.

Cline, on the other hand, by transforming the provided AWS_PROFILE into
a key / secret / session, as part of client initialization, completely
short-circuits this, making it very difficult for users in companies where
sessions are short-lived. Furthermore, Cline would silently ignore the
provided AWS_PROFILE if there isn't a current/non-expired session at the time
of initialization, pass null keys to the Bedrock SDK, which would then
make use the default profile, which may not be configured or authorized
to use AWS Bedrock (as most AWS SSO hub accounts would). From the
perspective of the user, this would manifest itself as an "supported
country" error or unhelpful errors that are basically impossible to
debug without attaching a debugger to Cline.. and such developers may
end up reaching out to their DevOps/IT teams for help, which also could
turn into a waste of time.

This PR addresses the aforementioned issues by resolving the credentials on
every invocation.

1: https://github.com/anthropics/anthropic-sdk-typescript/blob/61b55599d50d9c93840e4736cb756cb3a62b0696/packages/bedrock-sdk/src/auth.ts#L19
2025-02-20 22:41:29 +08:00
Saoud Rizwan 00506c0fcf Update version 2025-02-19 23:41:16 -08:00
Shawn Smith 45e1f58bbc Feat: MCP Marketplace Setting (#1877)
* feat-mc-marketplace-setting

* add changeset

* Fixes

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-19 23:40:22 -08:00
Saoud Rizwan cd3f3e2150 Update version 2025-02-19 18:58:03 -08:00
Saoud Rizwan ab8376fee4 Add 'Restore Task' option back to checkpoints 2025-02-19 18:57:16 -08:00
Saoud Rizwan ff7e5c8866 Update package 2025-02-19 18:03:11 -08:00
Saoud Rizwan 26be9469c4 Add delete button to MCP server 2025-02-19 18:02:20 -08:00
Saoud Rizwan a925e672ed Update MCP servers icon 2025-02-19 15:30:02 -08:00
Saoud Rizwan 2491b469e7 Remove downloadCount temporarily 2025-02-19 15:05:33 -08:00
Daniel Steigman 3fd1f16acb Merge pull request #1812 from Nick390/main
Add Arabic language
2025-02-19 12:10:17 -08:00
Saoud Rizwan 0a14f0bad5 Update version 2025-02-19 10:05:52 -08:00
Saoud Rizwan d72127550d Prepare for release 2025-02-19 09:20:44 -08:00
Saoud Rizwan 97fa8cffb9 Remove unnecessary recommended flag 2025-02-19 01:57:07 -08:00
Saoud Rizwan d6f51b4525 Fix tests 2025-02-18 22:17:01 -08:00
Saoud Rizwan c846bc4455 Adjust mermaid diagram colors 2025-02-18 22:14:36 -08:00
Saoud Rizwan efaa0d057e Add instruction about handling MCP servers with unexpected build path 2025-02-18 21:40:38 -08:00
Saoud Rizwan ceeb33fdc5 Fix race condition where MCP servers were sent to webview before it was initialized 2025-02-18 21:38:43 -08:00
Saoud Rizwan f938c40539 Fix MCP prompt to handle dist/index.js convention 2025-02-18 20:54:10 -08:00
Saoud Rizwan 65ac7ec3f7 Fix MCP empty list state 2025-02-18 20:04:17 -08:00
Saoud Rizwan f80552f92d Adjust MCP install prompt to demonstrate 2025-02-18 19:12:35 -08:00
Saoud Rizwan ac4ca51ced Center mermaid diagram 2025-02-18 18:54:30 -08:00
Saoud Rizwan 04f8c294c2 Fix mermaid width 2025-02-18 18:50:24 -08:00
Saoud Rizwan a6e109718e Remove logs 2025-02-18 18:30:36 -08:00
Saoud Rizwan f9025426b8 Toggle to act mode and enable MCP prompt if user one-click installs MCP server 2025-02-18 18:24:08 -08:00
Saoud Rizwan 97abdc500b Show pointer on mermaids 2025-02-18 18:03:08 -08:00
Saoud Rizwan 3d64ab298d Update zod 2025-02-18 17:33:48 -08:00
Saoud Rizwan ccdbe3a4a9 Prepare for release 2025-02-18 17:10:59 -08:00
Saoud Rizwan 000b4af458 Add getMcpServerDisplayName 2025-02-18 15:49:57 -08:00
Saoud Rizwan a01471f469 Show MCP server display name instead of ID 2025-02-18 15:37:42 -08:00
Saoud Rizwan d7788c2fd6 Fix mermaid diagram 2025-02-18 14:48:37 -08:00
Evan d6be431cb8 reopen img pr (#1850) 2025-02-18 14:24:14 -08:00
Evan 4c844cbafa Mermaid system prompt (#1849)
* wip

* install dependencies

* rendering mermaid graphs

* fix render failure on streaming

* fix bouncy screen by debouncing mermaid parsing

* add loading state; clean up styling

* replace tag symbols when rendering code

* better mermaid theme for visibility

* added changeset

* remove rehype-mermaid

* update webview package.lock

* clean up

* fix package-lock

* remove regular markdown background styling for mermaid blocks

* updated system prompt

* Update system.ts

* reopen pr

* revert random change

* random change

* reopen pr

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Evan Fannin <celestial_vault@Evans-MacBook-Pro.local>
2025-02-18 14:17:46 -08:00
Saoud Rizwan 9289d73a81 Prepare for release 2025-02-18 14:00:33 -08:00
nickbaumann98 19e5edbc25 Added raw cline-memory-bank custom instructions file to be used for "Add Memory Bank" functionality (#1832)
* Update README.md

* Create neat-apricots-search.md

* added cline-memory-bank markdown

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-18 12:35:40 -08:00
Kenji Hikmatullah a20a2444b3 Improve Mode Tooltip (#1839)
* feat: Improve Mode Tooltip

* Update webview-ui/src/components/chat/ChatTextArea.tsx

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

* fix: typo

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-18 12:33:21 -08:00
Evan cb02277b45 More Mermaids (#1833)
* wip

* install dependencies

* rendering mermaid graphs

* fix render failure on streaming

* fix bouncy screen by debouncing mermaid parsing

* add loading state; clean up styling

* replace tag symbols when rendering code

* better mermaid theme for visibility

* added changeset

* remove rehype-mermaid

* update webview package.lock

* clean up

* fix package-lock

* remove regular markdown background styling for mermaid blocks
2025-02-18 12:25:03 -08:00
Saoud Rizwan 220603e530 Update changelog 2025-02-18 10:59:43 -08:00
Saoud Rizwan e40e6ddbcd Refactor 2025-02-18 10:49:37 -08:00
Saoud Rizwan ec3bd56903 FIx outline 2025-02-17 22:15:43 -08:00
Saoud Rizwan c126804bb0 Add warning for community made servers; open GitHub on click 2025-02-17 22:01:26 -08:00
pashpashpash ee50e9710a Merge pull request #1841 from cline/pashpashpash/mcp-marketplace
Add MCP Marketplace
2025-02-17 20:43:12 -08:00
Saoud Rizwan 95acb1e95c Fix overflowing mcp server name 2025-02-17 20:34:29 -08:00
pashpashpash e587115d62 added silent fetching to when mcp view is clicked 2025-02-17 20:33:25 -08:00
pashpashpash 9c840fc89c Update webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-17 20:27:11 -08:00
Saoud Rizwan b7519a3669 Create stupid-mayflies-melt.md 2025-02-17 20:13:57 -08:00
Saoud Rizwan 691cf91a4d Fixes 2025-02-17 20:05:16 -08:00
Saoud Rizwan 24d3bfbccb Adjust styling 2025-02-17 19:50:35 -08:00
Saoud Rizwan 8927879d93 Refactor 2025-02-17 19:31:13 -08:00
pashpashpash 5f57415785 removed promise all 2025-02-17 19:08:29 -08:00
pashpashpash c3f7fa3386 cleaned up unused vars 2025-02-17 17:27:31 -08:00
pashpashpash e95cb30ebd merge conflicts resolved 2025-02-17 17:23:49 -08:00
pashpashpash 15b269c8f2 not using vscodelink just a tag 2025-02-17 16:39:40 -08:00
pashpashpash 31443f2a86 removed top border from submit card 2025-02-17 16:36:18 -08:00
pashpashpash 2d05dfd986 silent prefetching 2025-02-17 16:34:02 -08:00
pashpashpash 7722234761 submit card 2025-02-17 16:21:22 -08:00
pashpashpash 09c4202cb2 added submit card 2025-02-17 16:14:36 -08:00
pashpashpash 31eb212bee faster detail view 2025-02-17 15:03:10 -08:00
Daniel Trugman cc166f4144 Requesty provider improvements (#1829)
* Requesty: Add cline headers to requests

* Requesty: Add support for cache read/write tokens

* Requesty: Remove deepseek reasoner conversions that are handled in-flight by Requesty

* Requesty: Read o3-mini reasoning effort from Cline settings

* Requesty: Add total cost per interaction

* Add changeset
2025-02-17 10:37:23 -08:00
Saoud Rizwan 5dea79c65f Fix Mistral provider server URL 2025-02-17 10:35:21 -08:00
Saoud Rizwan 217162ccba Prepare for release 2025-02-16 21:01:52 -08:00
Saoud Rizwan a98269f6d5 Fix sending feedback with approve 2025-02-16 20:38:31 -08:00
Saoud Rizwan aab26e9a38 Fix empty message bubble after switching plan/act with a message 2025-02-16 15:38:06 -08:00
Saoud Rizwan 796eac558c Fix sending message when toggling plan/act mode 2025-02-16 15:15:27 -08:00
Evan 3873908efb Webpack Single Bundle (#1821)
* formatting

* changeset

---------

Co-authored-by: Evan Fannin <celestial_vault@Evans-MacBook-Pro.local>
2025-02-16 14:20:27 -08:00
Saoud Rizwan a6e812824c Give attribution to Szpadel for reasoning update 2025-02-15 23:42:12 -08:00
pashpashpash a4d1ae1af4 codegenicon renamed to codiconIcon 2025-02-15 19:00:08 -08:00
pashpashpash fa2797051d cleanup 2025-02-15 18:20:10 -08:00
pashpashpash 10315663b2 more cleanup 2025-02-15 17:37:02 -08:00
pashpashpash 338116fbd4 consistent spacing with flexbox gap 2025-02-15 17:23:08 -08:00
pashpashpash 89603d3e92 including llmsInstall + removed borders between items 2025-02-15 16:49:06 -08:00
Saoud Rizwan a3dc4471e3 Fixes 2025-02-15 16:45:54 -08:00
Saoud Rizwan 510bb72431 Disable markdown selectors 2025-02-15 15:31:45 -08:00
Saoud Rizwan 141a302521 Fix colors 2025-02-15 15:28:21 -08:00
Saoud Rizwan faae5f22a3 Open readme preview for mcp servers 2025-02-15 15:23:15 -08:00
Saoud Rizwan 0cbd6b321f update ui to look like vs marketplace 2025-02-15 14:33:58 -08:00
AlwaleedAlwabel c134291ba0 Merge branch 'main' of https://github.com/Nick390/cline 2025-02-15 13:55:20 +03:00
AlwaleedAlwabel 9b66eac9fe Create distance-fire-world.md 2025-02-15 13:54:59 +03:00
AlwaleedAlwabel c756470c51 Update locales/ar-sa/CODE_OF_CONDUCT.md
العرق = Race
الدين = Religion

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-15 13:43:20 +03:00
AlwaleedAlwabel b059b3ac1e Update README.md
**Fix mistakes in the translation.
2025-02-15 13:41:51 +03:00
AlwaleedAlwabel 7ae437fb94 Add Arabic language
**Add CODE_OF_CONDUCT in ar-sa.
**Add CONTRIBUTING in ar-sa.
**Add README in ar-sa.
2025-02-15 13:33:49 +03:00
Saoud Rizwan d67c7e36da Add git context mention (#1806)
* Add git context mention

* Fix context mention highlight

* Create long-guests-occur.md
2025-02-14 19:03:11 -08:00
Saoud Rizwan 584af64334 Add terminal context mention (#1805)
* Add terminal context mention

* Create fuzzy-moose-punch.md
2025-02-14 18:48:47 -08:00
pashpashpash e3b4b2c816 ui polish 2025-02-14 14:25:58 -08:00
pashpashpash 267b60c09e ui polish 2025-02-14 13:48:26 -08:00
stephen8339 ee06c0811c add all qwen2.5 coder models (#1797)
* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* Provide the api line choice for international user

* Remove redundant code

* Copy fixes

* Create dry-socks-talk.md

* fix problem what is when you use Qwen api provider and then you want to change the api provider ,the apiline dropdown will obscure your api provider drop-down options

* feat: add qwen2.5-coder models

Description

Add new models as list:
qwen2.5-coder-32b-instruct
qwen2.5-coder-14b-instruct
qwen2.5-coder-7b-instruct
qwen2.5-coder-3b-instruct

Doc: https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5u

* add changeset

* feat: add all alibaba qwen2.5 coder models

---------

Co-authored-by: yaojunWang <gooqle.com.hk@gmail.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: 刘耸 <song.liu@yo-star.com>
2025-02-14 11:10:37 -08:00
pashpashpash b8024497f2 mcp marketplace working 2025-02-13 19:11:08 -08:00
pashpashpash e3cfb405fb added some logging + error handling 2025-02-13 18:22:33 -08:00
wen-jy 8e12bdb00d Add support for qwen vl models (#1776)
* feat: add support for qwen vl models

* feat: add support for qwen vl models

* feat: updated the price of the Qianwen model following the BaiLian platform documentation

---------

Co-authored-by: 执无 <jiyong.wjy@alibaba-inc.com>
2025-02-13 18:03:20 -08:00
pashpashpash 7d67000d1f marketplace wip 2025-02-13 17:23:22 -08:00
Hiroki Nakashima 283d7d6928 fix: adjust litellm default context window settings (#1774) 2025-02-12 23:01:01 -08:00
Saoud Rizwan 83a28001be Fix plan/act response prompt 2025-02-12 20:18:36 -08:00
Gustavo de Oliveira 66d5a320b5 docs: Add translation to portuguese pt-BR (#1713)
* docs: Translate CONTRIBUTING to portuguese

* docs: Translate CODE_OF_CONDUCT to portuguese

* docs: Translate README to portuguese

* fix: typo errors

* fix: ellipses requested changes

* fix: Add changeset

* docs: add portuguese label in README.md
2025-02-12 18:18:46 -08:00
pashpashpash 95e1555361 Merge pull request #1748 from dcbartlett/feature/ENG-107_send_input_on_mode_switch
Feature/ENG_107 Send input on mode switch
2025-02-12 18:02:27 -08:00
Shawn Smith a99648884a Fix: Bedrock Profiles (#1751)
* fix-bedrock-profiles

* fix-bedrock-profiles

* resolve linting issue

* fixes

* resolve using var

* add changeset

* update changeset
2025-02-12 17:56:20 -08:00
brownrw8 0434b5c772 Advanced configuration for OpenAI Compatible Providers (#1737)
* feat: advanced configuration for OpenAI Compatible Providers

* Update .changeset/thirty-eyes-appear.md

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

* Update webview-ui/src/components/settings/ApiOptions.tsx

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

* dropdown menu

* Show pricing if user entered model info

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-12 14:09:55 -08:00
Sanjaykumar S e534c3dbc7 Add api key for litellm api provider #1766 (#1767)
* Add api key for litellm api provider

* Added Changeset

* Update litellm.ts

* Update ApiOptions.tsx

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-12 10:56:13 -08:00
pashpashpash add2f85053 Merge pull request #1768 from cline/celestial-vault/eng-198-accept-reject-diff-tool-with-user-feedback
Accept tool use with feedback
2025-02-12 10:21:42 -08:00
Evan 69ff71b051 add changeset 2025-02-12 08:56:29 -08:00
Evan f096d73813 approve with feedback for write_to_file/replace_in_file 2025-02-12 08:46:23 -08:00
Evan 96540ddf1e fix linting error 2025-02-11 13:21:22 -08:00
Evan 7da74bd9d8 add accept feedback 2025-02-11 13:20:54 -08:00
Dennis Bartlett 2a894d8cf1 Add Changeset 2025-02-11 04:34:43 -06:00
Dennis Bartlett 8793c2c6c5 fix: ESLint issue in BrowserSession 2025-02-11 04:29:09 -06:00
Dennis Bartlett 9e76cfd44a feat: Add SendMessage capability during Mode Switch 2025-02-11 04:26:35 -06:00
Saoud Rizwan 9c8254cf92 Revert context progress bar removal 2025-02-10 20:29:41 -08:00
dependabot[bot] 57ae7c05d6 Bump esbuild from 0.21.5 to 0.25.0 in the npm_and_yarn group (#1740)
Bumps the npm_and_yarn group with 1 update: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.21.5 to 0.25.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 20:04:47 -08:00
Waylon e8a2e88fa0 feat: qwen platform adds deepseek-r1/v3 support (#1729)
Co-authored-by: fine <fine_951111@163.com>
2025-02-10 20:04:16 -08:00
brownrw8 7b6a3d25e5 Advanced Setting to enable browser session (#1736)
* feat: add Advanced Setting to enable browser session

* ensure browser tool is removed from system prompt

* fix test

* Update BrowserSession.ts

* Update BrowserSession.ts

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-10 20:02:00 -08:00
supermomonga 82eba4d668 Add extension setting for chromium executable path (#1721)
* Add extension setting for chromium executable path

* apply changeset

* Update src/services/browser/BrowserSession.ts

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

* format code

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-10 00:13:48 -08:00
Saoud Rizwan 9812dd40c4 Fixes 2025-02-09 22:51:19 -08:00
Saoud Rizwan c29816c310 Create checkpoint at beginning of task 2025-02-09 22:44:35 -08:00
Saoud Rizwan 8938e02d8e Prepare for release 2025-02-09 03:25:13 -08:00
Saoud Rizwan fce7f1bea3 Use more visual checkpoints tracking 2025-02-09 03:03:09 -08:00
Saoud Rizwan fbd249d387 Adjust generation retry params 2025-02-09 02:14:34 -08:00
Saoud Rizwan 43200007cf Retry openrouter generation endpoint to fix context limit errors 2025-02-08 20:15:38 -08:00
Daniel Steigman 4d406fd989 Merge pull request #1715 from cline/clineignore-intro-markdown 2025-02-08 17:40:13 -08:00
Saoud Rizwan 68ab0cff25 Fix copy for clineignored scenarios 2025-02-08 17:15:42 -08:00
Evan 407788ee64 remove old changeset 2025-02-08 14:57:16 -08:00
Evan 41b2ad2b71 merge conflicts 2025-02-08 14:56:11 -08:00
Evan 1cc1c26585 adding changeset 2025-02-08 14:51:39 -08:00
Evan 18e51dff21 added explanatory markdown 2025-02-08 14:47:50 -08:00
Saoud Rizwan 02e45fed53 Re-arrange providers 2025-02-07 21:37:03 -08:00
Saoud Rizwan fa05f94dfa Fixes 2025-02-07 21:34:47 -08:00
Saoud Rizwan c1a7276867 Prepare for release 2025-02-07 21:22:55 -08:00
Saoud Rizwan 887456ead8 Fix bug where new files won't show up in files dropdown (#1704)
* Fix bug where new files won't show up in files dropdown

* Create small-gifts-count.md
2025-02-07 20:52:51 -08:00
brownrw8 134020d51b Add a keyboard shortcut to switch between plan and act mode (#1626)
* feat: add keyboard shortcut to Plan/Act toggle

* remove Shift

* fix: shortcut now Meta+Shift+a

* ENG-123:  Tooltip

* feat: tooltip and platform detection

* fix:impl suggestions

* fix: add changeset

* Fix style

* remove platform detection - use metaKey detection and os utils

* missed comma

* Fixes

* Fixes

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-07 19:29:54 -08:00
Evan 19c56c6ec4 Attention Is Not What You Need (#1680)
* initialize class

* wip

* wip

* cleaning up

* update comment

* fire n forget error handling

* files added

* changeset

* duplicate path function

* cleanup

* Use special error type for when cline tries to access clineignore'd file

* Rename LLMFileAccessController to ClineIgnoreController

* Use better changeset

* Remove unnecessary file parsing

* Allow access to files outside cwd

* Use more efficient clineIgnoreExists

* Modify clineignore prompt

* Use LOCK_TEXT_SYMBOL

* Block commands attempting to access clineignored files

* Fix prompt responses

* fix tests; make sure .clineignore is ignored

* Fix clineignore prompt

* Fixes

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-07 19:28:10 -08:00
Evan acd84e53a8 fix tests; make sure .clineignore is ignored 2025-02-07 15:31:09 -08:00
Evan 427e1edcfd Merge branch 'pass-controller-to-services' of https://github.com/cline/cline into pass-controller-to-services 2025-02-07 15:07:55 -08:00
Evan db3b356bb0 Merge branch 'main' into pass-controller-to-services 2025-02-07 14:38:42 -08:00
Evan 4449b51e2c Let's reason together (#1597)
* let's reason together

* typo

* changeset

* Fix type error

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-07 14:32:14 -08:00
brownrw8 84f017c98e feat:Add Together API Provider (#1698) 2025-02-07 13:55:48 -08:00
Saoud Rizwan b37f688755 Fix prompt responses 2025-02-07 12:11:14 -08:00
Saoud Rizwan 5585c684e9 Block commands attempting to access clineignored files 2025-02-07 11:46:44 -08:00
Saoud Rizwan 075e3171a8 Use LOCK_TEXT_SYMBOL 2025-02-07 10:52:57 -08:00
Saoud Rizwan 53604c9413 Modify clineignore prompt 2025-02-07 10:51:22 -08:00
Saoud Rizwan 8ae39e24be Use more efficient clineIgnoreExists 2025-02-07 10:33:47 -08:00
Saoud Rizwan 88c75a06f2 Allow access to files outside cwd 2025-02-07 00:58:20 -08:00
Saoud Rizwan 4f2d5b15bf Remove unnecessary file parsing 2025-02-07 00:37:03 -08:00
Saoud Rizwan f9e310f338 Use better changeset 2025-02-07 00:26:42 -08:00
Saoud Rizwan 7f292bc995 Rename LLMFileAccessController to ClineIgnoreController 2025-02-07 00:25:29 -08:00
Saoud Rizwan 88a07de5fb Use special error type for when cline tries to access clineignore'd file 2025-02-06 23:56:02 -08:00
Daniel Steigman 076b1e39e6 Nighttrek/bedrock credential manager (#1667)
* added initial AWS bedrock support

* fixed formatting

* updated to fix persistence

* added changeset
2025-02-06 23:36:38 -08:00
aicc 99bba4af27 fix problem what qwen api line drop-down options occlusion api provider drop-down options (#1688)
* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* Provide the api line choice for international user

* Remove redundant code

* Copy fixes

* Create dry-socks-talk.md

* fix problem what is when you use Qwen api provider and then you want to change the api provider ,the apiline dropdown will obscure your api provider drop-down options

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-06 23:04:43 -08:00
brownrw8 3cacd57949 Add dedicated Requesty provider (#1677)
* feat: Add dedicated Requesty provider

* Update ExtensionStateContext.tsx

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-06 20:45:52 -08:00
Evan fc60f6e55e cleanup 2025-02-06 18:49:11 -08:00
Saoud Rizwan 9bddd9a846 Change default OpenRouter model to non-moderated (#1683)
* Change default OR model to non-moderated

* Create breezy-bobcats-change.md
2025-02-06 18:43:38 -08:00
Evan 6cfdd4fb90 duplicate path function 2025-02-06 18:28:42 -08:00
Evan a31b394282 changeset 2025-02-06 18:09:20 -08:00
Evan 3866ae3449 merge conflicts 2025-02-06 18:05:29 -08:00
Evan 2a0558de80 files added 2025-02-06 14:51:43 -08:00
Matt Rubens 10b8ee4f54 Fix bug when shell profile is not found (#1671) 2025-02-06 12:12:25 -08:00
aicc 548338d39e Add alibaba qwen models plus/max/coder-plus/turbo both stable and latest to use. (#1648)
* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus  stable/latest models

* Provide the api line choice for international user

* Remove redundant code

* Copy fixes

* Create dry-socks-talk.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-05 23:23:33 -08:00
akfoster c88b5c67d5 Fix Changeset Test (#1657)
* should only pass if the PR has a new changeset

* improvements to docs-only check

* add changeset
2025-02-05 23:15:52 -08:00
nickbaumann98 1935aab2d5 Update README.md to include Getting Started (#1660)
* Update README.md

* Create neat-apricots-search.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-05 23:14:00 -08:00
Michael bd5eb8fcae feat: add retry decorator with rate limit handling (#1605)
* fix: improve retry decorator with smart rate limit handling

- Add handling of rate limit (429) errors
- Implement retry timing based on response headers
- Add exponential backoff when no headers present
- Add a few unit tests

Fixes #713

* Create modern-knives-tan.md

* Improve readability in retry.ts

---------

Co-authored-by: Michael Overhorst <m.overhorst@spotonmedics.nl>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-02-05 23:11:01 -08:00
Saoud Rizwan 0795b046e1 Prepare for release 2025-02-05 12:49:58 -08:00
Saoud Rizwan b2e4623559 Add new gemini models (#1656)
* Update gemini models

* Update changeset
2025-02-05 10:49:55 -08:00
omercelik 0b7fac0c9d Add Gemini 2.0 Pro (#1655) 2025-02-05 10:43:55 -08:00
Daniel Steigman 3fd0106816 Merge pull request #1643 from cline/dependabot/npm_and_yarn/webview-ui/npm_and_yarn-d9a8b47fa5
Bump vitest from 2.1.8 to 2.1.9 in /webview-ui in the npm_and_yarn group
2025-02-05 01:40:39 -08:00
Daniel Steigman b4dff4ec14 Merge pull request #1650 from cline/double_underscore_fix
Double underscore fix
2025-02-05 01:30:16 -08:00
Dennis Bartlett 9640a5f496 Merge branch 'main' of https://github.com/cline/cline into double_underscore_fix 2025-02-05 03:25:44 -06:00
Daniel Steigman f108f20466 updated the model list to remove the embedding model (#1646) 2025-02-05 01:11:08 -08:00
Daniel Steigman 905327db4e Merge pull request #1610 from ViezeVingertjes/feat/1609-add-mistral-models
feat: Add support for all available Mistral API models
2025-02-04 23:52:10 -08:00
dependabot[bot] 4d311ffb43 Bump vitest from 2.1.8 to 2.1.9 in /webview-ui in the npm_and_yarn group
Bumps the npm_and_yarn group in /webview-ui with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vitest` from 2.1.8 to 2.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-05 06:18:51 +00:00
Daniel Steigman 012e073fb9 Merge pull request #1641 from cline/ocasta181/ENG-182
ENG-182 | Add Changesets
2025-02-04 22:17:03 -08:00
Ocasta 6937823b69 use sys.exit, again 2025-02-04 22:05:03 -08:00
Ocasta 1415c8ce7b use sys.exit 2025-02-04 21:56:53 -08:00
Michael Overhorst fd676ef047 Removed /v1 from the url and tested several models. 2025-02-05 06:51:02 +01:00
Ocasta 9fbd8318f0 error handling when notes_start_index is invalid 2025-02-04 21:49:41 -08:00
Ocasta 15c147cf93 fix check changeset git action 2025-02-04 21:45:15 -08:00
Michael Overhorst 2cc0fa906c Updated the mistral url. 2025-02-05 06:41:00 +01:00
Ocasta 42c6dc7e94 update based on automated feedback 2025-02-04 19:20:18 -08:00
Ocasta 5db9ad2585 revert to older config 2025-02-04 19:06:34 -08:00
Ocasta 36d40afb4e changelogs 2025-02-04 18:58:51 -08:00
phadtrapong 01b1c8c0dd Fix installing-dev-essentials.md (#1600)
* Fix installing-dev-essentials.md

Fix installing-dev-essentials.md

* Fix link
2025-02-04 16:56:04 -08:00
Hiroki Nakashima 180fbd5995 feat: add LiteLLM API provider support (#1618) 2025-02-04 14:46:13 -08:00
watany 42924c971f chore: reduse cost openai (#1629) 2025-02-04 09:41:39 -08:00
Michael e41ff78770 feat:show api error status code (#1635)
* Show status code / message when API request error occurs.

* Moved logic to a helper method.

* Different error message format.

* Removed old comment.

---------

Co-authored-by: Michael Overhorst <m.overhorst@spotonmedics.nl>
2025-02-04 09:40:57 -08:00
Ofek Lev 6508549584 Update copyright year (#1637)
* Update copyright year

* Update LICENSE
2025-02-04 09:22:45 -08:00
Evan fa67db75e0 Add clineignore class into cline file (#1623) 2025-02-03 16:53:49 -08:00
Evan 989be56f4b fire n forget error handling 2025-02-03 13:56:04 -08:00
Evan 05093529bb update comment 2025-02-03 12:53:42 -08:00
Daniel Steigman c97fb242a9 Merge pull request #1469 from tszhong0411/fix-typos
fix: typos in docs and code
2025-02-03 11:56:56 -08:00
Evan fb613ef5c8 cleaning up 2025-02-03 08:47:57 -08:00
Michael Overhorst 466b1982f2 feat: Add support for all available Mistral API models
- Add all available Mistral models with specific version numbers
- Include Premier models (Mistral Large, Pixtral, Ministral, etc.)
- Include Free models (Mistral Small, Pixtral 12B, etc.)
- Set correct token limits and pricing for each model

Fixes #1609
2025-02-03 13:34:16 +01:00
Evan f6e64ccb93 wip 2025-02-02 14:19:12 -08:00
Saoud Rizwan 162cd3b9c5 Prepare for release 2025-02-01 18:32:48 -08:00
canvrno 5fd60b7000 Refactor Shell Detection to Use VS Code Terminal Profiles and Fallback Hierarchy (#1543)
* Provide explicit command chaining instructions

* Added shell detection for powershell
The default-shell library being used only returns cmd for windows users. This change will utilize VS Code API calls to determine the user's shell/terminal settings. MacOS & Linux will, for now, continue to use the existing method. Still working on tests.

* Replaced default-shell, added tests
Replaced default-shell with local code that replicates the old behavior on macOS & Linux
Windows shell detection uses VS Code settings to get the user's default terminal profile
Adjusted prompt change

* One small change

* Removed & attributed old package + typo

* Added VSC load for other OSes, refactor, better tests

* Fixed system.ts explicit git lines

* Added back changes for terminal-command-chaining

* One minor, but important change
2025-02-01 16:32:06 -08:00
Daniel Trugman a087f5e583 Fix reasoning_content check for openai & deepseek streams (#1594) 2025-02-01 16:15:57 -08:00
Evan 8e4ab703c8 wip 2025-01-31 16:57:24 -08:00
Saoud Rizwan 5eb8086b42 Add o3-mini support to OpenAI 2025-01-31 15:21:42 -08:00
Evan ef8d49c768 initialize class 2025-01-31 15:15:31 -08:00
Evan Fannin 2a078fee77 Class Implemented (#1577)
* wip

* LLMFileAccessController and tests

* added class and tests

* cleaning up

* formatting

* removing some defaults

* package json and remove defaults list
2025-01-31 12:09:12 -08:00
Saoud Rizwan 3df5e533b7 Prepare for release 2025-01-31 03:14:43 -08:00
Saoud Rizwan 03f07b7626 Fix model switching between plan/act; enable toggle and model switcher during generation 2025-01-31 03:08:24 -08:00
Saoud Rizwan 6500b7c210 Fixes 2025-01-31 02:38:27 -08:00
Saoud Rizwan 50f3791b98 Prepare for release 2025-01-31 02:37:13 -08:00
Saoud Rizwan 68ac266463 Add better support for r1 + show reasoning tokens 2025-01-31 02:35:06 -08:00
brownrw8 57bb43bb6f Basic unit testing for frontend /webview-ui (#1522)
* feat: very basic unit testing

* move dep -> devDeps

* correct other deps

* resync after i18n revert on main

* update package-lock.json
2025-01-30 21:48:19 -08:00
Saoud Rizwan 3b5d39a21b Prepare for release 2025-01-30 18:57:18 -08:00
Saoud Rizwan 4a8f3339fb Fixes 2025-01-30 18:55:49 -08:00
Saoud Rizwan 64718667ff Show context progress bar always 2025-01-30 18:55:03 -08:00
Saoud Rizwan 4f93437797 Fix creating github release 2025-01-30 18:49:39 -08:00
Saoud Rizwan 1cd2ff10c7 Prepare for release 2025-01-30 18:37:51 -08:00
tszhong0411 6bd6c830fe fix: typos in docs and code 2025-01-26 21:06:21 +08:00
Hanzen Shou 1ce57f2203 doc: linked issue to markdown plugin 2024-12-28 21:04:25 -08:00
Hanzen Shou c798df215e fix: double underscored filenames now render correctly 2024-12-28 21:03:19 -08:00
107 changed files with 11627 additions and 1304 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: make AWS Bedrock authentication predictable
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
A new MCP Marketplace display setting has been added to VSCode settings. (It is defaulted to "true")
+1
View File
@@ -21,6 +21,7 @@
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots
@@ -0,0 +1,103 @@
"""
This script updates a specific version's release notes section in CHANGELOG.md with new content
or reformats existing content.
The script:
1. Takes a version number, changelog path, and optionally new content as input from environment variables
2. Finds the section in the changelog for the specified version
3. Either:
a) Replaces the content with new content if provided, or
b) Reformats existing content by:
- Removing the first two lines of the changeset format
- Ensuring version numbers are wrapped in square brackets
4. Writes the updated changelog back to the file
Environment Variables:
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to update/format
PREV_VERSION: The previous version number (used to locate section boundaries)
NEW_CONTENT: Optional new content to insert for this version
"""
#!/usr/bin/env python3
import os
import sys
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
bracketed_version_pattern = f"## [{VERSION}]\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
# Try both unbracketed and bracketed version patterns
version_index = changelog_text.find(version_pattern)
if version_index == -1:
version_index = changelog_text.find(bracketed_version_pattern)
if version_index == -1:
# If version not found, add it at the top (after the first line)
first_newline = changelog_text.find('\n')
if first_newline == -1:
# If no newline found, just prepend
return f"## [{VERSION}]\n\n{changelog_text}"
return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}"
else:
# Using bracketed version
version_pattern = bracketed_version_pattern
notes_start_index = version_index + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
# Ensure we have at least 2 lines before removing them
if len(changeset_lines) < 2:
print("Warning: Changeset content has fewer than 2 lines")
parsed_lines = "\n".join(changeset_lines)
else:
# Remove the first two lines from the regular changeset format, ex: \n### Patch Changes
parsed_lines = "\n".join(changeset_lines[2:])
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
# Ensure version number is bracketed
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
print(f"Changelog content length: {len(changelog_content)} characters")
print("First 200 characters of changelog:")
print(changelog_content[:200])
print("----------------------------------------------------------------------------------")
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
print("New changelog content:")
print("----------------------------------------------------------------------------------")
print(new_changelog)
print("----------------------------------------------------------------------------------")
print(f"Writing updated changelog back to: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")
except FileNotFoundError:
print(f"Error: Changelog file not found at {CHANGELOG_PATH}")
sys.exit(1)
except Exception as e:
print(f"Error updating changelog: {str(e)}")
print(f"Current working directory: {os.getcwd()}")
sys.exit(1)
+162
View File
@@ -0,0 +1,162 @@
name: Changeset Release
run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }}
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
types: [closed, opened, labeled]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'cline-bot' ) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Git Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4
with:
node-version: 20
cache: "npm"
- name: Install Dependencies
run: npm run install:all
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
id: changesets
uses: changesets/action@e9cc34b540dd3ad1b030c57fd97269e8f6ad905a # v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 2: Process version bump PR created by cline-bot
changeset-pr-edit-approve:
name: Auto approve and merge Bump version PRs
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
github.actor == 'cline-bot' &&
contains(github.event.pull_request.title, 'Changeset version bump')
steps:
- name: Determine checkout ref
id: checkout-ref
run: |
echo "Event action: ${{ github.event.action }}"
echo "Actor: ${{ github.actor }}"
echo "Head ref: ${{ github.head_ref }}"
echo "PR SHA: ${{ github.event.pull_request.head.sha }}"
if [[ "${{ github.event.action }}" == "opened" && "${{ github.actor }}" == "cline-bot" ]]; then
echo "Using branch ref: ${{ github.head_ref }}"
echo "git_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT
else
echo "Using SHA ref: ${{ github.event.pull_request.head.sha }}"
echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi
- name: Checkout Repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.git_ref }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
run: |
git config user.name "cline-bot"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
git push
# Add label to indicate changelog has been formatted
- name: Add changelog-ready label
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['changelog-ready']
});
# Auto-approve PR only after it has been labeled
- name: Auto approve PR
if: contains(github.event.pull_request.labels.*.name, 'changelog-ready')
uses: hmarr/auto-approve-action@de8bf34d0402c38aa2c8346973342b2cb02c4435 # v4
with:
review-message: "I'm approving since it's a bump version PR"
# Auto-merge PR
- name: Automerge on PR
if: false # Needs enablePullRequestAutoMerge in repo settings to work contains(github.event.pull_request.labels.*.name, 'changelog-ready')
run: gh pr merge --auto --merge ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+117
View File
@@ -0,0 +1,117 @@
name: Check Changeset
run-name: Check for Changeset in PR
permissions:
contents: read
pull-requests: write
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
check-changeset:
# Skip draft PRs and dependabot PRs
if: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Check for changeset
id: check-changeset
run: |
# Debug info
echo "Current directory: $(pwd)"
echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}"
echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}"
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
echo "Git status:"
git status
# Get list of changed files
git fetch origin ${{ github.event.pull_request.base.ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
# Check if any of the changed files are in docs/ or .github/
echo "Checking if changes are docs-only..."
DOCS_ONLY=true
while IFS= read -r file; do
if [[ ! "$file" =~ ^(docs/|.github/) ]]; then
echo "Found non-docs change: $file"
DOCS_ONLY=false
break
fi
done <<< "$CHANGED_FILES"
# If changes are docs-only, skip changeset check
if [ "$DOCS_ONLY" = true ]; then
echo "All changes are in docs/ or .github/, skipping changeset check"
exit 0
else
echo "Changes include non-docs files, checking for changeset..."
fi
# Check if any changeset files are in the changed files
echo "Checking for changeset files in changed files..."
CHANGESET_IN_PR=false
while IFS= read -r file; do
if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" && "$file" != ".changeset/config.json" ]]; then
echo "Found changeset file in PR: $file"
CHANGESET_IN_PR=true
break
fi
done <<< "$CHANGED_FILES"
if [ "$CHANGESET_IN_PR" = false ]; then
echo "No changeset files found in changed files. Changed files in .changeset/:"
echo "$CHANGED_FILES" | grep "^\.changeset/" || true
echo "::error::No changeset file found in PR changes. Please run 'npm run changeset' to create one."
exit 1
fi
- name: Comment on PR
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const message = `This PR requires a changeset since it includes user-facing changes. Please:
1. Run \`npm run changeset\` locally
2. Choose the appropriate version bump:
- \`major\` for breaking changes (1.0.0 → 2.0.0)
- \`minor\` for new features (1.0.0 → 1.1.0)
- \`patch\` for bug fixes (1.0.0 → 1.0.1)
3. Write a clear description of your changes
4. Commit the generated changeset file
Note: Documentation-only changes do not require a changeset.`;
// Get existing comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Check if we already commented
const botComment = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('This PR requires a changeset')
);
if (!botComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}
+3
View File
@@ -70,8 +70,10 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Git Tag
id: create_tag
run: |
VERSION=v${{ steps.get_version.outputs.version }}
echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "Tagging with $VERSION"
git tag "$VERSION"
git push origin "$VERSION"
@@ -104,6 +106,7 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.create_tag.outputs.tag }}
files: "*.vsix"
# body: ${{ steps.changelog.outputs.content }}
generate_release_notes: true
+184 -130
View File
@@ -1,344 +1,398 @@
# Changelog
## [3.4.0]
- Introducing MCP Marketplace! You can now discover and install the best MCP servers right from within the extension, with new servers added regularly
- Add mermaid diagram support in Plan mode! You can now see visual representations of mermaid code blocks in chat, and click on them to see an expanded view
- Use more visual checkpoints indicators after editing files & running commands
- Create a checkpoint at the beginning of each task to easily revert to the initial state
- Add 'Terminal' context mention to reference the active terminal's contents
- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!)
- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.)
- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support
- Improve support for AWS Bedrock Profiles
- Fix Mistral provider support for non-codestral models
- Add advanced setting to disable browser tool
- Add advanced setting to set chromium executable path for browser tool
## [3.3.2]
- Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors
- Make checkpoints more visible and keep track of restored checkpoints
## [3.3.0]
- Add .clineignore to block Cline from accessing specified file patterns
- Add keyboard shortcut + tooltips for Plan/Act toggle
- Fix bug where new files won't show up in files dropdown
- Add automatic retry for rate limited requests (thanks @ViezeVingertjes!)
- Adding reasoning_effort support for o3-mini in Advanced Settings
- Added support for AWS provider profiles using the AWS CLI to make the profile, enabling long lived connections to AWS bedrock
- Adding Requesty API provider
- Add Together API provider
- Add Alibaba Qwen API provider (thanks @aicccode!)
## [3.2.13]
- Add new gemini models gemini-2.0-flash-lite-preview-02-05 and gemini-2.0-flash-001
- Add all available Mistral API models (thanks @ViezeVingertjes!)
- Add LiteLLM API provider support (thanks @him0!)
## [3.2.12]
- Fix command chaining for Windows users
- Fix reasoning_content error for OpenAI providers
## [3.2.11]
- Add OpenAI o3-mini model
## [3.2.10]
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct (thanks @Szpadel!)
- Show Reasoning tokens for models that support it
- Fix issues with switching models between Plan/Act modes
## [3.2.6]
- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode
- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases
- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese
- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!)
- Add Gemini 2.0 Flash Thinking experimental model
- Allow new users to subscribe to mailing list to get notified when new Accounts option is available
- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode
- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases
- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese
- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!)
- Add Gemini 2.0 Flash Thinking experimental model
- Allow new users to subscribe to mailing list to get notified when new Accounts option is available
## [3.2.5]
- Use yellow textfield outline in Plan mode to better distinguish from Act mode
- Use yellow textfield outline in Plan mode to better distinguish from Act mode
## [3.2.3]
- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!)
- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!)
## [3.2.0]
- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work
- Easily switch between API providers and models using a new popup menu under the chat field
- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together!
- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens!
- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens!
- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work
- Easily switch between API providers and models using a new popup menu under the chat field
- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together!
- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens!
- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens!
## [3.1.10]
- New icon!
- New icon!
## [3.1.9]
- Add Mistral API provider with codestral-latest model
- Add Mistral API provider with codestral-latest model
## [3.1.7]
- Add ability to change viewport size and headless mode when Cline asks to launch the browser
- Add ability to change viewport size and headless mode when Cline asks to launch the browser
## [3.1.6]
- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!)
- Update Anthropic model prices (thanks @timoteostewart!)
- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!)
- Update Anthropic model prices (thanks @timoteostewart!)
## [3.1.5]
- Fix bug where Cline couldn't read "@/" import path aliases from tool results
- Fix bug where Cline couldn't read "@/" import path aliases from tool results
## [3.1.4]
- Fix issue where checkpoints would not work for users with git commit signing enabled globally
- Fix issue where checkpoints would not work for users with git commit signing enabled globally
## [3.1.2]
- Fix issue where LFS files would be not be ignored when creating checkpoints
- Fix issue where LFS files would be not be ignored when creating checkpoints
## [3.1.0]
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
- Restore options: Choose to restore just the task state, just the workspace files, or both
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
- Task header now shows disk space usage with a delete button to help manage snapshot storage
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
- Restore options: Choose to restore just the task state, just the workspace files, or both
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
- Task header now shows disk space usage with a delete button to help manage snapshot storage
## [3.0.12]
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
## [3.0.11]
- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing
- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing
## [3.0.10]
- Add DeepSeek provider to API Provider options
- Fix context window limit errors for DeepSeek v3
- Add DeepSeek provider to API Provider options
- Fix context window limit errors for DeepSeek v3
## [3.0.9]
- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits
- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits
## [3.0.8]
- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks
- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks
## [3.0.7]
- Revert to using batched file watcher to fix crash when many files would be created at once
- Revert to using batched file watcher to fix crash when many files would be created at once
## [3.0.6]
- Fix bug where some files would be missing in the `@` context mention menu
- Add Bedrock support in additional regions
- Diff edit improvements
- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache)
- Fix bug where some files would be missing in the `@` context mention menu
- Add Bedrock support in additional regions
- Diff edit improvements
- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache)
## [3.0.4]
- Fix bug where gemini models would add code block artifacts to the end of text content
- Fix context mention menu visual issues on light themes
- Fix bug where gemini models would add code block artifacts to the end of text content
- Fix context mention menu visual issues on light themes
## [3.0.2]
- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for)
- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies
- Improves diff edit error handling
- Adds new Gemini models
- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for)
- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies
- Improves diff edit error handling
- Adds new Gemini models
## [3.0.0]
- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues.
- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't.
- Adds ability to enable system notifications for when Cline needs approval or completes a task.
- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project.
- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues.
- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't.
- Adds ability to enable system notifications for when Cline needs approval or completes a task.
- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project.
## [2.2.0]
- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool
- Add MCP server management tab accessible via the server icon in the menu bar
- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs")
- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool
- Add MCP server management tab accessible via the server icon in the menu bar
- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs")
## [2.1.6]
- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!)
- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!)
## [2.1.5]
- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`)
- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`)
## [2.1.4]
- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available)
- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available)
## [2.1.3]
- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence
- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence
## [2.1.2]
- Misc. bug fixes
- Update README with new browser feature
- Misc. bug fixes
- Update README with new browser feature
## [2.1.1]
- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser
- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser
## [2.1.0]
- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2)
- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2)
## [2.0.19]
- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter
- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter
## [2.0.18]
- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region)
- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region)
## [2.0.17]
- Update Anthropic model IDs
- Update Anthropic model IDs
## [2.0.16]
- Adjustments to system prompt
- Adjustments to system prompt
## [2.0.15]
- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits
- Fix bug where weaker models would display file contents before using the write_to_file tool
- Fix o1-mini and o1-preview errors when using OpenAI native
- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits
- Fix bug where weaker models would display file contents before using the write_to_file tool
- Fix o1-mini and o1-preview errors when using OpenAI native
## [2.0.14]
- Gracefully cancel requests while stream could be hanging
- Gracefully cancel requests while stream could be hanging
## [2.0.13]
- Detect code omission and show warning with troubleshooting link
- Detect code omission and show warning with troubleshooting link
## [2.0.12]
- Keep cursor out of the way during file edit streaming animation
- Keep cursor out of the way during file edit streaming animation
## [2.0.11]
- Adjust prompts around read_file to prevent re-reading files unnecessarily
- Adjust prompts around read_file to prevent re-reading files unnecessarily
## [2.0.10]
- More adjustments to system prompt to prevent lazy coding
- More adjustments to system prompt to prevent lazy coding
## [2.0.9]
- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`)
- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`)
## [2.0.8]
- Fix o1-mini and o1-preview for OpenAI
- Fix diff editor not opening sometimes in slow environments like project idx
- Fix o1-mini and o1-preview for OpenAI
- Fix diff editor not opening sometimes in slow environments like project idx
## [2.0.7]
- Misc. bug fixes
- Misc. bug fixes
## [2.0.6]
- Update URLs to https://github.com/cline/cline
- Update URLs to https://github.com/cline/cline
## [2.0.5]
- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file
- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads
- Fixed non-first chunk error handling in case stream fails mid-way through
- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file
- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads
- Fixed non-first chunk error handling in case stream fails mid-way through
## [2.0.0]
- New name! Meet Cline, an AI assistant that can use your CLI and Editor
- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files
- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks
- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models
- Search and use any model with OpenRouter
- New name! Meet Cline, an AI assistant that can use your CLI and Editor
- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files
- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks
- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models
- Search and use any model with OpenRouter
## [1.9.7]
- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules
- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules
## [1.9.6]
- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002`
- Updated system prompt to be more lenient when terminal output doesn't stream back properly
- Adjusted system prompt to prevent overuse of the inspect_site tool
- Increased global line height for improved readability
- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002`
- Updated system prompt to be more lenient when terminal output doesn't stream back properly
- Adjusted system prompt to prevent overuse of the inspect_site tool
- Increased global line height for improved readability
## [1.9.0]
- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own.
- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems.
- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own.
- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems.
## [1.8.0]
- You can now use '@' in the textarea to add context!
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
- @folder: Adds folder's files all at once to speed up your workflow even more
- You can now use '@' in the textarea to add context!
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
- @folder: Adds folder's files all at once to speed up your workflow even more
## [1.7.0]
- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.)
- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.)
## [1.6.5]
- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!)
- Task header can now be collapsed to provide more space for viewing conversations
- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks
- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!)
- Task header can now be collapsed to provide more space for viewing conversations
- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks
## [1.6.0]
- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files)
- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files)
## [1.5.27]
- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history!
- Updated system prompt to keep Claude from re-reading files unnecessarily
- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history!
- Updated system prompt to keep Claude from re-reading files unnecessarily
## [1.5.19]
- Adds support for OpenAI compatible API providers (e.g. Ollama!)
- Adds support for OpenAI compatible API providers (e.g. Ollama!)
## [1.5.13]
- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments)
- Adds search to Task History
- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments)
- Adds search to Task History
## [1.5.6]
- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans)
- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans)
## [1.5.4]
- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website")
- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website")
## [1.5.0]
- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
## [1.4.0]
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
- Implement sliding window context management to keep tasks going past 200k tokens
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
- Implement sliding window context management to keep tasks going past 200k tokens
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
## [1.3.0]
- Adds task history
- Adds task history
## [1.2.0]
- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku)
- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku)
## [1.1.1]
- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
- Adds option to add custom instructions to the end of the system prompt
- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
- Adds option to add custom instructions to the end of the system prompt
## [1.1.0]
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
## [1.0.9]
- Add support for OpenRouter and AWS Bedrock
- Add support for OpenRouter and AWS Bedrock
## [1.0.8]
- Shows diff view of new or edited files right in the editor
- Shows diff view of new or edited files right in the editor
## [1.0.7]
- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task
- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task
## [1.0.6]
- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers
- Export tasks to markdown files (useful as context for future tasks)
- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers
- Export tasks to markdown files (useful as context for future tasks)
## [1.0.5]
- Claude now has context about vscode's visible editors and opened tabs
- Claude now has context about vscode's visible editors and opened tabs
## [1.0.4]
- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly
- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure
- Provide feedback to tool use like terminal commands and file edits
- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`)
- Added ability to retry failed API requests (helpful for rate limits)
- Quality of life improvements like markdown rendering, memory optimizations, better theme support
- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly
- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure
- Provide feedback to tool use like terminal commands and file edits
- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`)
- Added ability to retry failed API requests (helpful for rate limits)
- Quality of life improvements like markdown rendering, memory optimizations, better theme support
## [0.0.6]
- Initial release
- Initial release
+13 -3
View File
@@ -56,20 +56,30 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Update existing tests if your changes affect them
- Include both unit tests and integration tests where appropriate
4. **Commit Guidelines**
4. **Version Management with Changesets**
- Create a changeset for any user-facing changes using `npm run changeset`
- Choose the appropriate version bump:
- `major` for breaking changes (1.0.0 → 2.0.0)
- `minor` for new features (1.0.0 → 1.1.0)
- `patch` for bug fixes (1.0.0 → 1.0.1)
- Write clear, descriptive changeset messages that explain the impact
- Documentation-only changes don't require changesets
5. **Commit Guidelines**
- Write clear, descriptive commit messages
- Use conventional commit format (e.g., "feat:", "fix:", "docs:")
- Reference relevant issues in commits using #issue-number
5. **Before Submitting**
6. **Before Submitting**
- Rebase your branch on the latest main
- Ensure your branch builds successfully
- Double-check all tests are passing
- Review your changes for any debugging code or console logs
6. **Pull Request Description**
7. **Pull Request Description**
- Clearly describe what your changes do
- Include steps to test the changes
- List any breaking changes
+2 -2
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Cline Bot Inc.
Copyright 2025 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -198,4 +198,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
+2 -2
View File
@@ -24,7 +24,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>We're Hiring!</strong></a>
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
@@ -187,4 +187,4 @@ To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.m
## License
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+1 -1
View File
@@ -28,7 +28,7 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
- **Interested in contributing?** We welcome your input:
- Feel free to submit a pull request
- [Contribution Guidelines](CONTRIBUTING.md)
- [Contribution Guidelines](../CONTRIBUTING.md)
## Additional Resources
+54
View File
@@ -0,0 +1,54 @@
### .clineignore Support
To give you more control over which files are accessible to Cline, we've implemented `.clineignore` functionality, similar to `.gitignore`. This allows you to specify files and directories that Cline should **not** access or process. This is useful for:
* **Privacy:** Preventing Cline from accessing sensitive or private files in your workspace.
* **Performance:** Excluding large directories or files that are irrelevant to your tasks, potentially improving the efficiency of Cline.
* **Context Management:** Focusing Cline's attention on the relevant parts of your project.
**How to use `.clineignore`**
1. **Create a `.clineignore` file:** In the root directory of your workspace (the same level as your `.vscode` folder, or the top level folder you opened in VS Code), create a new file named `.clineignore`.
2. **Define ignore patterns:** Open the `.clineignore` file and specify the patterns for files and directories you want Cline to ignore. The syntax is the same as `.gitignore`:
* Each line in the file represents a pattern.
* **Standard glob patterns are supported:**
* `*` matches zero or more characters
* `?` matches one character
* `[]` matches a character range
* `**` matches any number of directories and subdirectories.
* **Directory patterns:** Append `/` to the end of a pattern to specify a directory.
* **Negation patterns:** Start a pattern with `!` to negate (un-ignore) a previously ignored pattern.
* **Comments:** Start a line with `#` to add comments.
**Example `.clineignore` file:**
```
# Ignore log files
*.log
# Ignore the entire 'node_modules' directory
node_modules/
# Ignore all files in the 'temp' directory and its subdirectories
temp/**
# But DO NOT ignore 'important.log' even if it's in the root
!important.log
# Ignore any file named 'secret.txt' in any subdirectory
**/secret.txt
```
3. **Cline respects your `.clineignore`:** Once you save the `.clineignore` file, Cline will automatically recognize and apply these rules.
* **File Access Control:** Cline will not be able to read the content of ignored files using tools like `read_file`. If you attempt to use a tool on an ignored file, Cline will inform you that access is blocked due to `.clineignore` settings.
* **File Listing:** When you ask Cline to list files in a directory (e.g., using `list_files`), ignored files and directories will still be listed, but they will be marked with a **🔒** symbol next to their name to indicate that they are ignored. This helps you understand which files Cline can and cannot interact with.
4. **Dynamic Updates:** Cline monitors your `.clineignore` file for changes. If you modify, create, or delete your `.clineignore` file, Cline will automatically update its ignore rules without needing to restart VS Code or the extension.
**In Summary**
The `.clineignore` file provides a powerful and flexible way to control Cline's access to your workspace files, enhancing privacy, performance, and context management. By leveraging familiar `.gitignore` syntax, you can easily tailor Cline's focus to the most relevant parts of your projects.
@@ -102,4 +102,4 @@ The **Problems** section in VS Code shows any errors or warnings in your code. Y
## Next Steps
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](getting-started-new-coders.md) guide to continue your journey.
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](../getting-started-new-coders/README.md) guide to continue your journey.
+2 -2
View File
@@ -38,7 +38,7 @@ STOP! Before proceeding, you MUST verify these requirements:
<img src="https://github.com/user-attachments/assets/abf908b1-be98-4894-8dc7-ef3d27943a47" alt="MCP Server Panel" width="400" />
1. The MCP settings files should be display in a tab in VS Code.
1. Replce the file's contents with this code:
1. Replace the file's contents with this code:
For Windows:
@@ -96,7 +96,7 @@ You should witness Cline:
1. Update the mcp setting json file
1. Start the server and start the server
The mcp seetings file should now look like this:
The mcp settings file should now look like this:
_For a Windows machine:_
@@ -0,0 +1,153 @@
# Cline's Memory Bank
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
## Memory Bank Structure
The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
```mermaid
flowchart TD
PB[projectbrief.md] --> PC[productContext.md]
PB --> SP[systemPatterns.md]
PB --> TC[techContext.md]
PC --> AC[activeContext.md]
SP --> AC
TC --> AC
AC --> P[progress.md]
```
### Core Files (Required)
1. `projectbrief.md`
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `productContext.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `activeContext.md`
- Current work focus
- Recent changes
- Next steps
- Active decisions and considerations
4. `systemPatterns.md`
- System architecture
- Key technical decisions
- Design patterns in use
- Component relationships
5. `techContext.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
6. `progress.md`
- What works
- What's left to build
- Current status
- Known issues
### Additional Context
Create additional files/folders within memory-bank/ when they help organize:
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core Workflows
### Plan Mode
```mermaid
flowchart TD
Start[Start] --> ReadFiles[Read Memory Bank]
ReadFiles --> CheckFiles{Files Complete?}
CheckFiles -->|No| Plan[Create Plan]
Plan --> Document[Document in Chat]
CheckFiles -->|Yes| Verify[Verify Context]
Verify --> Strategy[Develop Strategy]
Strategy --> Present[Present Approach]
```
### Act Mode
```mermaid
flowchart TD
Start[Start] --> Context[Check Memory Bank]
Context --> Update[Update Documentation]
Update --> Rules[Update .clinerules if needed]
Rules --> Execute[Execute Task]
Execute --> Document[Document Changes]
```
## Documentation Updates
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user requests with **update memory bank** (MUST review ALL files)
4. When context needs clarification
```mermaid
flowchart TD
Start[Update Process]
subgraph Process
P1[Review ALL Files]
P2[Document Current State]
P3[Clarify Next Steps]
P4[Update .clinerules]
P1 --> P2 --> P3 --> P4
end
Start --> Process
```
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
## Project Intelligence (.clinerules)
The .clinerules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.
```mermaid
flowchart TD
Start{Discover New Pattern}
subgraph Learn [Learning Process]
D1[Identify Pattern]
D2[Validate with User]
D3[Document in .clinerules]
end
subgraph Apply [Usage]
A1[Read .clinerules]
A2[Apply Learned Patterns]
A3[Improve Future Work]
end
Start --> Learn
Learn --> Apply
```
### What to Capture
- Critical implementation paths
- User preferences and workflow
- Project-specific patterns
- Known challenges
- Evolution of project decisions
- Tool usage patterns
The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .clinerules as a living document that grows smarter as we work together.
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
+47
View File
@@ -0,0 +1,47 @@
# ميثاق المساهمين
## تعهدنا
نحن المساهمون والقائمون على هذا المشروع، نتعهد بتوفير بيئة مفتوحة ومرحبة، ونجعل المشاركة في مشروعنا ومجتمعنا تجربة خالية من التحرش للجميع، بغض النظر عن العمر، أو حجم الجسم، أو الإعاقة، أو العرق، أو الخصائص الجنسية، أو الهوية الجنسية والتعبير عنها، أو مستوى الخبرة، أو التعليم، أو الوضع الاجتماعي والاقتصادي، أو الجنسية، أو المظهر الشخصي، أو الدين، أو الهوية الجنسية والتوجه الجنسي.
## معاييرنا
أمثلة على السلوك الذي يساهم في خلق بيئة إيجابية تشمل:
- استخدام لغة ترحيبية وشاملة
- احترام وجهات النظر والخبرات المختلفة
- تقبل النقد البناء برحابة صدر
- التركيز على ما هو الأفضل للمجتمع
- إظهار التعاطف تجاه أعضاء المجتمع الآخرين
أمثلة على السلوك غير المقبول من قبل المشاركين تشمل:
- استخدام لغة أو صور جنسية والاهتمام الجنسي غير المرغوب فيه أو التحرش الجنسي
- التصيد، والتعليقات المهينة/المسيئة، والهجمات الشخصية أو السياسية
- التحرش العلني أو الخاص
- نشر معلومات الآخرين الخاصة، مثل العنوان الفعلي أو الإلكتروني، دون إذن صريح
- أي سلوك آخر يمكن اعتباره غير لائق في بيئة مهنية
## مسؤولياتنا
يتحمل القائمون على المشروع مسؤولية توضيح معايير السلوك المقبول، ومن المتوقع أن يتخذوا إجراءات تصحيحية مناسبة وعادلة استجابة لأي حالات سلوك غير مقبول.
يحق للقائمين على المشروع إزالة أو تعديل أو رفض التعليقات والالتزامات والتعليمات البرمجية وتعديلات wiki والمشكلات والمساهمات الأخرى التي لا تتماشى مع مدونة قواعد السلوك هذه، أو حظر أي مساهم بشكل مؤقت أو دائم بسبب سلوكيات أخرى يعتبرونها غير لائقة أو مهددة أو مسيئة أو ضارة، كما أنهم يتحملون مسؤولية ذلك.
## النطاق
تنطبق مدونة قواعد السلوك هذه داخل مساحات المشروع وفي الأماكن العامة عندما يمثل الفرد المشروع أو مجتمعه. تتضمن أمثلة تمثيل مشروع أو مجتمع استخدام عنوان بريد إلكتروني رسمي للمشروع، أو النشر عبر حساب رسمي على وسائل التواصل الاجتماعي، أو العمل كممثل معين في حدث عبر الإنترنت أو خارجه. يمكن للقائمين على المشروع تحديد وتوضيح تمثيل المشروع بشكل أكبر.
## التنفيذ
يمكن الإبلاغ عن حالات السلوك المسيء أو التحرش أو السلوك غير المقبول عن طريق الاتصال بفريق المشروع على hi@cline.bot. ستتم مراجعة جميع الشكاوى والتحقيق فيها وستؤدي إلى استجابة تعتبر ضرورية ومناسبة للظروف. يلتزم فريق المشروع بالحفاظ على السرية فيما يتعلق بالمبلغ عن الحادث. يمكن نشر مزيد من التفاصيل حول سياسات التنفيذ المحددة بشكل منفصل.
قد يواجه القائمون على المشروع الذين لا يتبعون أو يفرضون مدونة قواعد السلوك بحسن نية تداعيات مؤقتة أو دائمة على النحو الذي يحدده الأعضاء الآخرون في قيادة المشروع.
## الإسناد
تم اقتباس مدونة قواعد السلوك هذه من [تعهد المساهم][homepage]، الإصدار 1.4، متاح على https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
للحصول على إجابات للأسئلة الشائعة حول مدونة قواعد السلوك هذه، راجع https://www.contributor-covenant.org/faq
+93
View File
@@ -0,0 +1,93 @@
# المساهمة في Cline
نحن سعداء لاهتمامك بالمساهمة في Cline. سواء كنت تصلح خطأً أو تضيف ميزة أو تحسن الوثائق لدينا، فإن كل مساهمة تجعل Cline أذكى! للحفاظ على مجتمعنا نابضًا بالحياة وترحيبيًا، يجب على جميع الأعضاء الالتزام بـ [مدونة قواعد السلوك](CODE_OF_CONDUCT.md) لدينا.
## الإبلاغ عن الأخطاء أو المشكلات
تساعد تقارير الأخطاء على جعل Cline أفضل للجميع! قبل إنشاء مشكلة جديدة، يرجى [البحث عن المشكلات الموجودة](https://github.com/cline/cline/issues) لتجنب الازدواجية. عندما تكون جاهزًا للإبلاغ عن خطأ، انتقل إلى [صفحة المشكلات](https://github.com/cline/cline/issues/new/choose) حيث ستجد قالبًا لمساعدتك في ملء المعلومات ذات الصلة.
<blockquote class='warning-note'>
🔐 <b>مهم:</b> إذا اكتشفت ثغرة أمنية، فيرجى استخدام <a href="https://github.com/cline/cline/security/advisories/new">أداة الأمان على Github للإبلاغ عنها بشكل خاص</a>.
</blockquote>
## تحديد ما يجب العمل عليه
تبحث عن مساهمة أولى جيدة؟ تحقق من المشكلات المميزة بـ ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) أو ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). تم تحديد هذه المشكلات خصيصًا للمساهمين الجدد والمجالات التي نرحب فيها بالمساعدة!
نرحب أيضًا بالمساهمات في [الوثائق](https://github.com/cline/cline/tree/main/docs) لدينا! سواء كان تصحيح أخطاء إملائية، أو تحسين الأدلة الحالية، أو إنشاء محتوى تعليمي جديد - نود بناء مستودع موارد مدفوع من المجتمع يساعد الجميع على الاستفادة القصوى من Cline. يمكنك البدء بالغوص في `/docs` والبحث عن مجالات تحتاج إلى تحسين.
إذا كنت تخطط للعمل على ميزة أكبر، فيرجى إنشاء [طلب ميزة](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) أولاً حتى نتمكن من مناقشة ما إذا كان ذلك يتماشى مع رؤية Cline.
## إعداد التطوير
1. **إضافات VS Code**
- عند فتح المشروع، سيطالبك VS Code بتثبيت الإضافات الموصى بها
- هذه الإضافات مطلوبة للتطوير - يرجى قبول جميع مطالبات التثبيت
- إذا تجاهلت المطالبات، يمكنك تثبيتها يدويًا من لوحة الإضافات
2. **التطوير المحلي**
- قم بتشغيل `npm run install:all` لتثبيت التبعيات
- قم بتشغيل `npm run test` لتشغيل الاختبارات محليًا
- قبل تقديم طلب السحب، قم بتشغيل `npm run format:fix` لتنسيق التعليمات البرمجية الخاصة بك
## كتابة وتقديم التعليمات البرمجية
يمكن لأي شخص المساهمة بالتعليمات البرمجية في Cline، لكننا نطلب منك اتباع هذه الإرشادات لضمان دمج مساهماتك بسلاسة:
1. **احتفظ بطلبات السحب مركزة**
- قيد طلبات السحب بميزة واحدة أو إصلاح خطأ
- قسم التغييرات الأكبر إلى طلبات سحب أصغر ومتصلة
- قسم التغييرات إلى التزامات منطقية يمكن مراجعتها بشكل مستقل
2. **جودة التعليمات البرمجية**
- قم بتشغيل `npm run lint` للتحقق من نمط التعليمات البرمجية
- قم بتشغيل `npm run format` لتنسيق التعليمات البرمجية تلقائيًا
- يجب أن تجتاز جميع طلبات السحب عمليات التحقق المستمر التي تشمل كلاً من التنضيد والتنسيق
- تعامل مع أي تحذيرات أو أخطاء ESLint قبل التقديم
- اتبع أفضل ممارسات TypeScript والحفاظ على سلامة النوع
3. **الاختبار**
- أضف اختبارات للميزات الجديدة
- قم بتشغيل `npm test` للتأكد من اجتياز جميع الاختبارات
- قم بتحديث الاختبارات الحالية إذا كانت تغييراتك تؤثر عليها
- تضمين كل من اختبارات الوحدة واختبارات التكامل حيثما كان ذلك مناسبًا
4. **إدارة الإصدار مع Changesets**
- أنشئ changeset لأي تغييرات واجهة المستخدم باستخدام `npm run changeset`
- اختر زيادة الإصدار المناسبة:
- `major` للتغييرات الكبيرة (1.0.0 → 2.0.0)
- `minor` للميزات الجديدة (1.0.0 → 1.1.0)
- `patch` لإصلاحات الأخطاء (1.0.0 → 1.0.1)
- اكتب رسائل changeset واضحة ووصفية تشرح التأثير
- لا تتطلب التغييرات في الوثائق فقط changesets
5. **إرشادات الالتزام (Commit Guidelines)**
- اكتب رسائل التزام واضحة وواصفة
- استخدم تنسيق الالتزام التقليدي (مثل: "feat:", "fix:", "docs:")
- أشر إلى القضايا ذات الصلة في الالتزامات باستخدام #رقم-القضية
6. **قبل الإرسال**
- قم بإعادة دمج فرعك مع أحدث إصدار من الفرع الرئيسي
- تأكد من أن الفرع الخاص بك يُبنى بنجاح
- تحقق من اجتياز جميع الاختبارات
- راجع التغييرات الخاصة بك للتأكد من عدم وجود تعليمات تصحيح الأخطاء أو سجلات وحدة التحكم
7. **وصف طلب السحب (Pull Request Description)**
- صف بوضوح ما تقوم به التغييرات
- قم بتضمين خطوات لاختبار التغييرات
- أدرج أي تغييرات غير متوافقة
- أضف لقطات شاشة للتغييرات في واجهة المستخدم
## اتفاقية المساهمة
من خلال إرسال طلب سحب، فإنك توافق على أن مساهماتك سيتم ترخيصها بنفس ترخيص المشروع ([Apache 2.0](LICENSE)).
تذكر: المساهمة في Cline لا تقتصر فقط على كتابة الكود - إنها تتعلق بأن تكون جزءًا من مجتمع يُشكل مستقبل التطوير بمساعدة الذكاء الاصطناعي. لنبنِ شيئًا رائعًا معًا! 🚀
+189
View File
@@ -0,0 +1,189 @@
<div align="center"><sub>
العربية | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">الإسبانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">الألمانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">اليابانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">الصينية المبسطة</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">الصينية التقليدية</a> | <a href="https://github.com/cline/cline/blob/main/locales/pt-BR/README.md" target="_blank">البرتغالية</a>
</sub></div>
# Cline \#1 على OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>تنزيل من متجر VS</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>طلبات الميزات</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>البدء</strong></a>
</td>
</tbody>
</table>
</div>
التقى Cline، مساعد الذكاء الاصطناعي الذي يمكنه استخدام **سطر الأوامر** و **محرر النصوص** الخاص بك.
بفضل [قدرات Claude 3.5 Sonnet على التعليمات البرمجية الوكيلة](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
1. أدخل مهمتك وأضف الصور لتحويل المحاكاة إلى تطبيقات وظيفية أو إصلاح الأخطاء مع لقطات الشاشة.
2. يبدأ Cline بتحليل هيكل الملفات الخاصة بك وشجرة التعريف المصدرية، وإجراء عمليات بحث regex، وقراءة الملفات ذات الصلة للاطلاع على المشاريع الحالية. من خلال إدارة المعلومات التي يتم إضافتها إلى السياق بعناية، يمكن لـ Cline تقديم مساعدة قيمة حتى للمشاريع الكبيرة والمعقدة دون إرهاق نافذة السياق.
3. بمجرد حصول Cline على المعلومات التي يحتاجها، يمكنه:
- إنشاء وتعديل الملفات + مراقبة أخطاء Linter/Compiler أثناء السير، مما يسمح له بإصلاح المشكلات مثل الواردات المفقودة وأخطاء البناء النحوي بمفرده.
- تنفيذ الأوامر مباشرة في الطرفية الخاصة بك ومراقبة إخراجها أثناء العمل، مما يسمح له على سبيل المثال بالاستجابة لمشكلات خادم التطوير بعد تعديل ملف.
- بالنسبة لمهام تطوير الويب، يمكن لـ Cline إطلاق الموقع في متصفح بلا رأس، والنقر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة + سجلات وحدة التحكم، مما يسمح له بإصلاح أخطاء وقت التشغيل والأخطاء البصرية.
4. عند اكتمال المهمة، سيقدم Cline النتيجة لك مع أمر طرفية مثل `open -a "Google Chrome" index.html`، والذي تقوم بتشغيله بنقرة زر.
> [!TIP]
> استخدم اختصار `CMD/CTRL + Shift + P` لفتح لوحة الأوامر واكتب "Cline: Open In New Tab" لفتح الإضافة كعلامة تبويب في محرر النصوص الخاص بك. يتيح لك هذا استخدام Cline جنبًا إلى جنب مع مستكشف الملفات الخاص بك، ورؤية كيف يغير مساحة العمل الخاصة بك بوضوح أكبر.
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
### استخدم أي واجهة برمجة تطبيقات ونموذج
يدعم Cline مقدمي واجهات برمجة التطبيقات مثل OpenRouter و Anthropic و OpenAI و Google Gemini و AWS Bedrock و Azure و GCP Vertex. يمكنك أيضًا تكوين أي واجهة برمجة تطبيقات متوافقة مع OpenAI، أو استخدام نموذج محلي من خلال LM Studio/Ollama. إذا كنت تستخدم OpenRouter، فستقوم الإضافة بجلب قائمة النماذج الأحدث الخاصة بهم، مما يسمح لك باستخدام أحدث النماذج بمجرد توفرها.
تتتبع الإضافة أيضًا إجمالي الرموز والاستخدام الخاص بواجهة برمجة التطبيقات لدورة المهمة بأكملها وطلبات فردية، مما يبقيك على اطلاع بالإنفاق في كل خطوة.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### تشغيل الأوامر في الطرفية
بفضل [تحديثات تكامل الشل الجديدة في VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)، يمكن لـ Cline تنفيذ الأوامر مباشرة في الطرفية الخاصة بك وتلقي الإخراج. يسمح له هذا بأداء مجموعة واسعة من المهام، من تثبيت الحزم وتشغيل سكربتات البناء إلى نشر التطبيقات، وإدارة قواعد البيانات، وتنفيذ الاختبارات، وذلك بالتكيف مع بيئة التطوير الخاصة بك وسلسلة الأدوات للقيام بالعمل على النحو الصحيح.
بالنسبة للعمليات الطويلة المدى مثل خوادم التطوير، استخدم زر "المتابعة أثناء التشغيل" للسماح لـ Cline بالاستمرار في المهمة بينما يعمل الأمر في الخلفية. أثناء عمل Cline، سيتم إخباره بأي إخراج طرفية جديد على الطريق، مما يسمح له بالاستجابة للمشكلات التي قد تنشأ، مثل أخطاء وقت الإنشاء عند تعديل الملفات.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### إنشاء وتعديل الملفات
يمكن لـ Cline إنشاء وتعديل الملفات مباشرة في محرر النصوص الخاص بك، وعرض الاختلافات. يمكنك تعديل أو إلغاء تغييرات Cline مباشرة في محرر الاختلافات، أو تقديم ملاحظات في الدردشة حتى تكون راضيًا عن النتيجة. يراقب Cline أيضًا أخطاء Linter/Compiler (الواردات المفقودة، أخطاء البناء النحوي، إلخ) حتى يتمكن من إصلاح المشكلات التي تنشأ أثناء السير بمفرده.
يتم تسجيل جميع التغييرات التي أجراها Cline في جدول زمني للملف، مما يوفر طريقة سهلة لتتبع وإلغاء التعديلات إذا لزم الأمر.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### استخدم المتصفح
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 3.5 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
حاول طلب من Cline "اختبار التطبيق"، وشاهده يشغل أمرًا مثل `npm run dev`، ويطلق خادم التطوير المحلي في متصفح، ويجري سلسلة من الاختبارات للتأكد من أن كل شيء يعمل. [شاهد عرضًا توضيحيًا هنا.](https://x.com/sdrzn/status/1850880547825823989)
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### "إضافة أداة التي..."
شكراً لـ [بروتوكول سياق النموذج](https://github.com/modelcontextprotocol)، يمكن لـ Cline توسيع قدراته من خلال الأدوات المخصصة. بينما يمكنك استخدام [الخوادم التي أنشأها المجتمع](https://github.com/modelcontextprotocol/servers)، يمكن لـ Cline بدلاً من ذلك إنشاء أدوات وتثبيتها مصممة خصيصًا لتناسب سير عملك. ما عليك سوى أن تطلب من Cline "إضافة أداة"، وسيتولى كل شيء، من إنشاء خادم MCP جديد إلى تثبيته في الامتداد. تصبح هذه الأدوات المخصصة بعد ذلك جزءًا من مجموعة أدوات Cline، جاهزة للاستخدام في المهام المستقبلية.
- **"أضف أداة تجلب تذاكر Jira"**: استرجع تذاكر AC وقم بتشغيل Cline
- **"أضف أداة تدير AWS EC2s"**: تحقق من مقاييس الخادم وقم بتوسيع أو تقليص عدد الحالات
- **"أضف أداة تجلب أحدث حوادث PagerDuty"**: استرجع التفاصيل واطلب من Cline إصلاح الأخطاء
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### إضافة السياق
**`@url`**: الصق رابط URL ليقوم الامتداد بجلبه وتحويله إلى Markdown، مفيد عندما تريد تزويد Cline بأحدث الوثائق
**`@problems`**: أضف أخطاء وتحذيرات بيئة العمل ('لوحة المشكلات') ليتمكن Cline من إصلاحها
**`@file`**: يضيف محتويات ملف حتى لا تضطر إلى إهدار طلبات API بالموافقة على قراءة الملف (+ البحث في الملفات)
**`@folder`**: يضيف جميع ملفات المجلد دفعة واحدة لتسريع سير العمل بشكل أكبر
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### نقاط التحقق: المقارنة والاستعادة
أثناء عمل Cline على مهمة، يأخذ الامتداد لقطة من بيئة العمل في كل خطوة. يمكنك استخدام زر "Compare" لرؤية الفرق بين اللقطة وبيئة العمل الحالية، وزر "Restore" للعودة إلى تلك النقطة.
على سبيل المثال، عند العمل مع خادم ويب محلي، يمكنك استخدام "استعادة بيئة العمل فقط" لاختبار إصدارات مختلفة من تطبيقك بسرعة، ثم استخدام "استعادة المهمة وبيئة العمل" عندما تجد الإصدار الذي تريد المتابعة منه. يتيح لك ذلك استكشاف أساليب مختلفة بأمان دون فقدان التقدم.
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## المساهمة
للمساهمة في المشروع، ابدأ بـ [دليل المساهمة](CONTRIBUTING.md) لتعلم الأساسيات. يمكنك أيضًا الانضمام إلى [خادم Discord](https://discord.gg/cline) للدردشة مع المساهمين الآخرين في قناة `#contributors`. إذا كنت تبحث عن عمل بدوام كامل، تحقق من الوظائف المتاحة على [صفحة التوظيف](https://cline.bot/join-us)!
<details>
<summary>تعليمات التطوير المحلي</summary>
1. استنساخ المستودع _(يتطلب [git-lfs](https://git-lfs.com/))_:
```bash
git clone https://github.com/cline/cline.git
```
2. افتح المشروع في VSCode:
```bash
code cline
```
3. قم بتثبيت التبعيات اللازمة للامتداد وواجهة الويب:
```bash
npm run install:all
```
4. قم بالتشغيل بالضغط على `F5` (أو من `Run` -> `Start Debugging`) لفتح نافذة VSCode جديدة مع تحميل الامتداد. (قد تحتاج إلى تثبيت [إضافة esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) إذا واجهت مشكلات في بناء المشروع.)
</details>
<details>
<summary>إنشاء طلب سحب (Pull Request)</summary>
1. قبل إنشاء PR، قم بإنشاء إدخال للتغييرات:
```bash
npm run changeset
```
سيطلب منك تحديد:
- نوع التغيير (رئيسي، ثانوي، إصلاح)
- `رئيسي` → تغييرات غير متوافقة (1.0.0 → 2.0.0)
- `ثانوي` → ميزات جديدة (1.0.0 → 1.1.0)
- `إصلاح` → إصلاحات للأخطاء (1.0.0 → 1.0.1)
- وصف التغييرات التي قمت بها
2. قم بحفظ التغييرات وملف `.changeset` الذي تم إنشاؤه
3. ادفع فرعك وأنشئ PR على GitHub. سيقوم CI بـ:
- تشغيل الاختبارات والفحوصات
- سيقوم Changesetbot بإنشاء تعليق يوضح تأثير الإصدار
- عند الدمج مع الفرع الرئيسي، سيقوم Changesetbot بإنشاء PR لحزم الإصدار
- عند دمج PR لحزم الإصدار، سيتم نشر إصدار جديد
</details>
## الرخصة
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+51
View File
@@ -0,0 +1,51 @@
# Código de Conduta para Contribuidores
## Nosso Compromisso
Com o objetivo de promover um ambiente aberto e acolhedor, nós, como contribuidores e mantenedores, nos comprometemos a tornar a participação em nosso projeto e comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou orientação sexual.
## Nossos Padrões
Exemplos de comportamentos que contribuem para criar um ambiente positivo incluem:
- Uso de linguagem acolhedora e inclusiva
- Respeito por diferentes pontos de vista e experiências
- Aceitar críticas de maneira construtiva
- Foco no que é melhor para a comunidade
- Ser empático com outros membros da comunidade
Exemplos de comportamentos inaceitáveis por parte dos participantes incluem:
- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais indesejados
- Trollar, insultar, fazer comentários depreciativos, ataques pessoais ou políticos
- Assédio público ou privado
- Divulgar informações privadas sem autorização, como endereços físicos ou eletrônicos, sem permissão explícita
- Outras condutas que poderiam ser consideradas inadequadas em um ambiente profissional
## Nossas Responsabilidades
Os mantenedores do projeto são responsáveis por esclarecer os padrões de comportamento aceitáveis e devem tomar ações corretivas apropriadas e justas em resposta a qualquer instância de comportamento inaceitável.
Os mantenedores têm o direito e a responsabilidade de remover, editar ou rejeitar comentários, commits, códigos, edições no wiki, issues e outras contribuições que não estejam alinhadas com este Código de Conduta. Também podem banir temporária ou permanentemente qualquer colaborador cujo comportamento seja considerado inapropriado, ameaçador, ofensivo ou prejudicial.
## Escopo
Este Código de Conduta se aplica tanto aos espaços do projeto quanto aos espaços públicos
quando uma pessoa representa o projeto ou sua comunidade. Exemplos de
representação de um projeto ou comunidade incluem o uso de um endereço de e-mail oficial do projeto,
publicar em uma conta oficial de mídia social ou atuar como representante designado
em um evento online ou offline. A representação de um projeto pode
ser mais especificamente definido e esclarecido pelos mantenedores do projeto.
## Aplicação
Casos de comportamento abusivo, assediador ou inaceitáveis podem ser reportados entrando em contato com a equipe do projeto pelo email hi@cline.bot. Todas as queixas serão revisadas e investigadas confidencialmente. Mais detalhes sobre políticas específicas podem ser publicados separadamente.
Os mantenedores que não seguirem ou aplicarem este Código de Conduta de boa fé podem enfrentar repercussões temporárias ou permanentes determinadas por outros membros da liderança do projeto.
## Atribuição
Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 1.4, disponível em https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.
+83
View File
@@ -0,0 +1,83 @@
# Contribuir para o Cline
Estamos felizes por você estar interessado em contribuir com o Cline. Seja corrigindo um erro, adicionando uma funcionalidade ou melhorando nossa documentação, cada contribuição torna o Cline mais inteligente! Para manter nossa comunidade viva e acolhedora, todos os membros devem cumprir nosso Código de Conduta [Código de Conduta](CODE_OF_CONDUCT.md).
## Relatar erros ou problemas
Relatar erros ajuda a melhorar o Cline para todos! Antes de criar um novo issue, revise as [issues existentes](https://github.com/cline/cline/issues) para evitar duplicações. Quando estiver pronto para relatar um erro, vá até nossa [página de Issues](https://github.com/cline/cline/issues/new/choose), onde você encontrará um modelo que ajudará a preencher as informações relevantes.
<blockquote class='warning-note'>
🔐 <b>Importante:</b> Se você descobrir uma vulnerabilidade de segurança, utilize a <a href="https://github.com/cline/cline/security/advisories/new">ferramenta de segurança do GitHub</a> para relatá-la de forma privada.
</blockquote>
## Escolher no que trabalhar
Procurando uma boa primeira contribuição? Consulte os problemas marcados com ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) ou ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). Estes foram especialmente selecionados para novos colaboradores e são áreas em que adoraríamos receber ajuda!
Também damos boas-vindas a contribuições para nossa [documentação](https://github.com/cline/cline/tree/main/docs). Seja corrigindo erros de digitação, melhorando guias existentes ou criando novos conteúdos educativos, queremos construir um repositório de recursos gerido pela comunidade que ajude todos a tirar o máximo proveito do Cline. Você pode começar explorando `/docs` e procurando áreas que precisam de melhorias.
Se planeja trabalhar em uma funcionalidade maior, crie primeiro uma [solicitação de funcionalidade](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) para que possamos discutir se ela se alinha à visão do Cline.
## Configurar o ambiente de desenvolvimento
1. **Extensões do VS Code**
- Ao abrir o projeto, o VS Code solicitará que você instale as extensões recomendadas.
- Essas extensões são necessárias para o desenvolvimento aceite todas as solicitações de instalação.
- Caso tenha rejeitado as solicitações, você pode instalá-las manualmente na seção de extensões.
2. **Desenvolvimento local**
- Execute `npm run install:all` para instalar as dependências.
- Execute `npm run test` para rodar os testes localmente.
- Antes de enviar um PR, execute `npm run format:fix` para formatar seu código.
## Escrever e enviar código
Qualquer pessoa pode contribuir com código para o Cline, mas pedimos que siga estas diretrizes para garantir que suas contribuições sejam integradas sem problemas:
1. **Mantenha os Pull Requests focados**
- Limite os PRs a uma única funcionalidade ou correção de erro.
- Divida alterações maiores em PRs menores e coerentes.
- Divida as alterações em commits lógicos que possam ser revisados independentemente.
2. **Qualidade do código**
- Execute `npm run lint` para verificar o estilo do código.
- Execute `npm run format` para formatar automaticamente o código.
- Todos os PRs devem passar nas verificações do CI, que incluem linting e formatação.
- Resolva todos os avisos ou erros do ESLint antes de enviar.
- Siga as melhores práticas para TypeScript e mantenha a segurança dos tipos.
3. **Testes**
- Adicione testes para novas funcionalidades.
- Execute `npm test` para garantir que todos os testes passem.
- Atualize testes existentes caso suas alterações os afetem.
- Inclua tanto testes unitários quanto de integração onde for apropriado.
4. **Diretrizes de commits**
- Escreva mensagens de commit claras e descritivas.
- Use o formato convencional (por exemplo, "feat:", "fix:", "docs:").
- Faça referência aos issues relevantes nos commits usando #número-do-issue.
5. **Antes de enviar**
- Faça rebase com sua branch com a última versão da branch principal (main).
- Certifique-se de que sua branch seja construída corretamente.
- Verifique se todos os testes passam.
- Revise suas alterações para remover qualquer código de depuração ou logs desnecessários.
6. **Descrição do Pull Request**
- Descreva claramente o que suas alterações fazem.
- Inclua passos para testar as alterações.
- Liste quaisquer mudanças importantes.
- Adicione capturas de tela para mudanças na interface do usuário.
## Acordo de contribuição
Ao enviar um Pull Request, você concorda que suas contribuições serão licenciadas sob a mesma licença do projeto ([Apache 2.0](LICENSE)).
Lembre-se: Contribuir com o Cline não é apenas escrever código é fazer parte de uma comunidade que está moldando o futuro do desenvolvimento assistido por IA. Vamos criar algo incrível juntos! 🚀
+161
View File
@@ -0,0 +1,161 @@
# Cline #1 no OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Baixar no VS Marketplace</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Solicitação de Funcionalidades</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>Estamos Contratando!</strong></a>
</td>
</tbody>
</table>
</div>
Conheça o Cline: um assistente de IA que pode usar seu **CLI** e **Editor**.
Graças às [habilidades avançadas do Claude 3.5 Sonnet](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
1. Insira sua tarefa e adicione imagens para transformar mockups em aplicativos funcionais ou corrigir erros através de capturas de tela.
2. O Cline começará analisando a estrutura do seu arquivo e os ASTs do código-fonte, fazendo pesquisas com Regex e lendo arquivos relevantes para se orientar em projetos existentes. Ao gerenciar cuidadosamente as informações agregadas, o Cline pode fornecer assistência valiosa mesmo em projetos grandes e complexos, sem sobrecarregar a janela de contexto.
3. Assim que ele tiver as informações necessárias, o Cline poderá:
- Criar e editar arquivos + monitorar erros de Linter/Compilador, para que você possa corrigir proativamente problemas como importações ausentes e erros de sintaxe.
- Executar comandos diretamente no terminal e monitorar o resultado, para que você possa responder a problemas do servidor de desenvolvimento após editar um arquivo.
- Para tarefas de desenvolvimento web, o Cline pode iniciar o site em um navegador headless, clicar, digitar, fazer scroll e capturar capturas de tela + registros de console, para que você possa corrigir erros em tempo de execução e erros visuais.
> [!TIP]
> Use o atalho de teclado `CMD/CTRL + Shift + P` para abrir a lista de comandos possiveis e digite "Cline: Abrir em nova aba" para abrir a extensão como uma aba no seu editor. Dessa forma, você pode usar o Cline junto com seu explorador de arquivos e ver mais claramente como seu espaço de trabalho muda.
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
### Use qualquer API ou modelo
O Cline oferece suporte a provedores de API como OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure e GCP Vertex. Você também pode configurar qualquer API compatível com OpenAI ou usar um modelo local via LM Studio/Ollama. Se você usar o OpenRouter, a extensão recuperará sua lista de modelos mais recentes, para que você possa usar os modelos mais novos assim que estiverem disponíveis.
A extensão também rastreia o uso total de tokens e os custos da API para todo o ciclo de tarefas e solicitações individuais, para que você seja informado sobre as despesas em cada etapa.
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### Executar comandos no terminal
Graças às novas [atualizações de integração do Shell no VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), o Cline pode executar comandos diretamente no seu terminal e receber o resultado. Isso permite que você execute uma variedade de tarefas, desde instalar pacotes e executar build scripts para fazer deploy de aplicações, gerenciar bancos de dados e executar testes, adaptando-se ao seu ambiente de desenvolvimento e ferramentas para fazer o trabalho corretamente.
Para processos de longa duração, como servidores de desenvolvimento, use o botão "Continuar durante a execução" para permitir que o Cline continue a tarefa enquanto o comando é executado em segundo plano. Enquanto Cline trabalha, você será notificado sobre novas saídas do terminal, para que possa responder a problemas que possam surgir, como erros de compilação ao editar arquivos.
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### Criar e editar arquivos
Cline pode criar e editar arquivos diretamente no seu editor, apresentando um diff com as alterações. Você pode editar ou reverter as alterações do Cline diretamente no editor de diff ou fornecer feedback no chat até ficar satisfeito com o resultado. Cline também monitora erros de linter/compilador (importações ausentes, erros de sintaxe, etc.) para que possa corrigir problemas que surgem ao longo do caminho por conta própria.
Todas as alterações feitas pelo Cline são registradas na Linha do tempo do arquivo, fornecendo uma maneira fácil de rastrear e reverter modificações, caso seja necessário.
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### Uso do navegador
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 3.5, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
Tente pedir a Cline para "testar o aplicativo" e observe enquanto o Cline executa um comando como `npm run dev`, inicia seu servidor de desenvolvimento local em um navegador e executa uma série de testes para confirmar se tudo funciona. [Veja uma demonstração aqui.](https://x.com/sdrzn/status/1850880547825823989)
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### "adicione uma ferramenta que..."
Graças ao [Model Context Protocol](https://github.com/modelcontextprotocol), o Cline pode expandir seus recursos por meio de ferramentas personalizadas. Embora você possa usar [servidores criados pela comunidade](https://github.com/modelcontextprotocol/servers), Cline pode criar e instalar ferramentas especificamente para seu fluxo de trabalho. Basta pedir ao Cline para "adicionar uma ferramenta" e ele cuidará de tudo, desde a criação de um novo servidor MCP até a instalação na extensão. Essas ferramentas personalizadas se tornam parte do conjunto de ferramentas da Cline e estão prontas para serem usadas em tarefas futuras.
- "adicione uma ferramenta que recupere tickets do Jira": Recupere ACs de tickets e coloque Cline para trabalhar
- "adicione uma ferramenta que gerencie AWS EC2s": verifique as métricas do servidor e aumente ou diminua as instâncias
- "adicione uma ferramenta para recuperar os últimos incidentes do PagerDuty": Recupere detalhes e peça ao Cline para corrigir erros
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### Adicione contexto
**`@url`:** Insira uma URL para a extensão recuperar e converter para Markdown, que é útil quando você deseja fornecer ao Cline documentos mais recentes
**`@problems`:** Adicionar erros e avisos do espaço de trabalho (painel 'Problemas') que o Cline deve corrigir
**`@file`:** Adicione o conteúdo de um arquivo para que você não precise desperdiçar solicitações de API para aprovar a leitura do arquivo (+ para pesquisar arquivos)
**`@folder`:** Adicione arquivos de uma pasta por vez para acelerar ainda mais seu fluxo de trabalho
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### Checkpoints: Comparar e Restaurar
Enquanto Cline trabalha em uma tarefa, a extensão cria um instantâneo de seu espaço de trabalho em cada etapa. Você pode usar o botão "Comparar" para ver a diferença entre o instantâneo e seu espaço de trabalho atual, e o botão "Restaurar" para retornar a esse ponto.
Por exemplo, se estiver trabalhando com um servidor web local, você pode usar 'Restaurar somente o espaço de trabalho' para testar rapidamente diferentes versões do seu aplicativo e, em seguida, 'Restaurar tarefa e espaço de trabalho' quando encontrar a versão na qual deseja continuar trabalhando. Isso permite que você explore diferentes abordagens com segurança sem perder o progresso.
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Contribuições
Para contribuir com o projeto, comece com nosso [Guia de Contribuição](CONTRIBUTING.md) para aprender o básico. Você também pode entrar no nosso [Discord](https://discord.gg/cline) para bater papo com outros colaboradores no canal `#contributors`. Se você está procurando um emprego de período integral, confira nossas vagas em aberto na nossa [página de carreiras](https://cline.bot/join-us).
<details>
<summary>Instruções para desenvolvimento local</summary>
1. Clone o repositório _(Necessário [git-lfs](https://git-lfs.com/))_:
```bash
git clone https://github.com/cline/cline.git
```
2. Abra o projeto no VSCode:
```bash
code cline
```
3. Instale as dependências necessárias para a extensão e webview-gui:
```bash
npm run install:all
```
4. Inicie pressionando `F5` (ou `Executar`->`Iniciar Depuração`) para abrir uma nova janela do VSCode com a extensão carregada. (Pode ser necessário instalar a [extensão esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) se você encontrar problemas ao compilar seu projeto.)
</details>
## Licença
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
+553 -74
View File
@@ -1,19 +1,19 @@
{
"name": "claude-dev",
"version": "3.2.6",
"version": "3.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.2.6",
"version": "3.4.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
"@anthropic-ai/vertex-sdk": "^0.4.1",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
@@ -32,10 +32,11 @@
"firebase": "^11.2.0",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"ignore": "^7.0.3",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"openai": "^4.61.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
@@ -47,7 +48,7 @@
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.23.8"
"zod": "^3.24.2"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
@@ -62,7 +63,7 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"esbuild": "^0.21.5",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
@@ -2532,10 +2533,78 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [
"arm64"
],
@@ -2546,7 +2615,347 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -2610,6 +3019,15 @@
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3660,6 +4078,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@manypkg/get-packages/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/@manypkg/get-packages/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -3671,13 +4098,25 @@
}
},
"node_modules/@mistralai/mistralai": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz",
"integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==",
"dependencies": {
"zod-to-json-schema": "^3.24.1"
},
"peerDependencies": {
"zod": ">= 3"
}
},
"node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
},
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
@@ -5564,12 +6003,6 @@
"integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
"license": "MIT"
},
"node_modules/@types/should": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@types/should/-/should-11.2.0.tgz",
@@ -5634,6 +6067,15 @@
}
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
@@ -5772,6 +6214,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -6442,6 +6893,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -6618,6 +7070,15 @@
"devtools-protocol": "*"
}
},
"node_modules/chromium-bidi/node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -6992,6 +7453,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -7373,6 +7835,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
@@ -7385,6 +7848,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7437,9 +7901,9 @@
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -7447,32 +7911,34 @@
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
"@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm": "0.25.0",
"@esbuild/android-arm64": "0.25.0",
"@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-arm64": "0.25.0",
"@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-s390x": "0.25.0",
"@esbuild/linux-x64": "0.25.0",
"@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/netbsd-x64": "0.25.0",
"@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/openbsd-x64": "0.25.0",
"@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
}
},
"node_modules/escalade": {
@@ -7628,6 +8094,15 @@
"node": ">=10.13.0"
}
},
"node_modules/eslint/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -8260,6 +8735,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -8407,6 +8883,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -8604,6 +9081,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"engines": {
"node": ">= 4"
}
},
"node_modules/google-auth-library": {
"version": "9.14.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.0.tgz",
@@ -8624,6 +9109,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
@@ -8681,6 +9167,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -8693,6 +9180,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8705,6 +9193,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8739,6 +9228,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -8915,10 +9405,9 @@
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"license": "MIT",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
"integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
"engines": {
"node": ">= 4"
}
@@ -10495,6 +10984,7 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10558,28 +11048,30 @@
}
},
"node_modules/openai": {
"version": "4.61.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.61.0.tgz",
"integrity": "sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==",
"version": "4.83.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz",
"integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"@types/qs": "^6.9.15",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"qs": "^6.10.3"
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
@@ -11276,21 +11768,6 @@
"node": ">=18"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11740,6 +12217,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -11887,6 +12365,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
@@ -13340,9 +13819,9 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+34 -7
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.2.7",
"version": "3.4.5",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -73,7 +73,7 @@
{
"command": "cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(server)"
"icon": "$(extensions)"
},
{
"command": "cline.historyButtonClicked",
@@ -161,6 +161,31 @@
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task."
},
"cline.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disables extension from spawning browser session."
},
"cline.modelSettings.o3Mini.reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"default": "medium",
"description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses."
},
"cline.chromeExecutablePath": {
"type": "string",
"default": null,
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
},
"cline.mcpMarketplace.enabled": {
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
}
}
}
@@ -187,7 +212,8 @@
"publish:marketplace": "vsce publish && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
"prepare": "husky",
"changeset": "changeset"
"changeset": "changeset",
"version-packages": "changeset version"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
@@ -202,7 +228,7 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"esbuild": "^0.21.5",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
@@ -215,7 +241,7 @@
"@anthropic-ai/sdk": "^0.26.0",
"@anthropic-ai/vertex-sdk": "^0.4.1",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
@@ -234,10 +260,11 @@
"firebase": "^11.2.0",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"ignore": "^7.0.3",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"openai": "^4.61.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
@@ -249,6 +276,6 @@
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.23.8"
"zod": "^3.24.2"
}
}
+12
View File
@@ -11,8 +11,12 @@ import { GeminiHandler } from "./providers/gemini"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { RequestyHandler } from "./providers/requesty"
import { TogetherHandler } from "./providers/together"
import { QwenHandler } from "./providers/qwen"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { LiteLlmHandler } from "./providers/litellm"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -46,10 +50,18 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new OpenAiNativeHandler(options)
case "deepseek":
return new DeepSeekHandler(options)
case "requesty":
return new RequestyHandler(options)
case "together":
return new TogetherHandler(options)
case "qwen":
return new QwenHandler(options)
case "mistral":
return new MistralHandler(options)
case "vscode-lm":
return new VsCodeLmHandler(options)
case "litellm":
return new LiteLlmHandler(options)
default:
return new AnthropicHandler(options)
}
+2
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { withRetry } from "../retry"
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
@@ -16,6 +17,7 @@ export class AnthropicHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let stream: AnthropicStream<Anthropic.Beta.PromptCaching.Messages.RawPromptCachingBetaMessageStreamEvent>
+71 -31
View File
@@ -3,49 +3,25 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "../"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: AnthropicBedrock
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new AnthropicBedrock({
// Authenticate by either providing the keys below or use the default AWS credential providers, such as
// using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables.
...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}),
...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}),
...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}),
// awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION,
// and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region.
awsRegion: this.options.awsRegion,
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// cross region inference requires prefixing the model id with the region
let modelId: string
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
switch (regionPrefix) {
case "us-":
modelId = `us.${this.getModel().id}`
break
case "eu-":
modelId = `eu.${this.getModel().id}`
break
default:
// cross region inference is not supported in this region, falling back to default model
modelId = this.getModel().id
break
}
} else {
modelId = this.getModel().id
}
let modelId = await this.getModelId()
const stream = await this.client.messages.create({
// create anthropic client, using sessions created or renewed after this handler's
// initialization, and allowing for session renewal if necessary as well
let client = await this.getClient()
const stream = await client.messages.create({
model: modelId,
max_tokens: this.getModel().info.maxTokens || 8192,
temperature: 0,
@@ -112,4 +88,68 @@ export class AwsBedrockHandler implements ApiHandler {
info: bedrockModels[bedrockDefaultModelId],
}
}
private async getClient(): Promise<AnthropicBedrock> {
// Create AWS credentials by executing a an AWS provider chain exactly as the
// Anthropic SDK does it, by wrapping the default chain into a temporary process
// environment.
const providerChain = fromNodeProviderChain()
const credentials = await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
},
() => providerChain(),
)
// Return an AnthropicBedrock client with the resolved/assumed credentials.
//
// When AnthropicBedrock creates its AWS client, the chain will execute very
// fast as the access/secret keys will already be already provided, and have
// a higher precedence than the profiles.
return new AnthropicBedrock({
awsAccessKey: credentials.accessKeyId,
awsSecretKey: credentials.secretAccessKey,
awsSessionToken: credentials.sessionToken,
awsRegion: this.options.awsRegion || "us-east-1",
})
}
private async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
switch (regionPrefix) {
case "us-":
return `us.${this.getModel().id}`
case "eu-":
return `eu.${this.getModel().id}`
break
default:
// cross region inference is not supported in this region, falling back to default model
return this.getModel().id
break
}
}
return this.getModel().id
}
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
const previousEnv = { ...process.env }
try {
updateEnv()
return await fn()
} finally {
process.env = previousEnv
}
}
private static async setEnv(key: string, value: string | undefined) {
if (key !== "" && value !== undefined) {
process.env[key] = value
}
}
}
+23 -1
View File
@@ -1,9 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -17,12 +19,25 @@ export class DeepSeekHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
// Only set temperature for non-reasoner models
@@ -38,6 +53,13 @@ export class DeepSeekHandler implements ApiHandler {
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
+2
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GoogleGenerativeAI } from "@google/generative-ai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "../../shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
@@ -17,6 +18,7 @@ export class GeminiHandler implements ApiHandler {
this.client = new GoogleGenerativeAI(options.geminiApiKey)
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.client.getGenerativeModel({
model: this.getModel().id,
+60
View File
@@ -0,0 +1,60 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
const stream = await this.client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [systemMessage, ...formattedMessages],
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: liteLlmModelInfoSaneDefaults,
}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
@@ -21,11 +22,11 @@ export class MistralHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
serverURL: "https://codestral.mistral.ai",
apiKey: this.options.mistralApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat.stream({
model: this.getModel().id,
+29
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
@@ -10,6 +11,7 @@ import {
} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -22,6 +24,7 @@ export class OpenAiNativeHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
switch (this.getModel().id) {
case "o1":
@@ -43,6 +46,32 @@ export class OpenAiNativeHandler implements ApiHandler {
}
break
}
case "o3-mini": {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
break
}
default: {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
+22 -3
View File
@@ -1,9 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -26,13 +28,22 @@ export class OpenAiHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: this.options.openAiModelId ?? "",
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
@@ -46,6 +57,14 @@ export class OpenAiHandler implements ApiHandler {
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
@@ -59,7 +78,7 @@ export class OpenAiHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.openAiModelId ?? "",
info: openAiModelInfoSaneDefaults,
info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults,
}
}
}
+76 -20
View File
@@ -1,11 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import delay from "delay"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import delay from "delay"
import { convertToR1Format } from "../transform/r1-format"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -23,11 +25,12 @@ export class OpenRouterHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
@@ -98,6 +101,15 @@ export class OpenRouterHandler implements ApiHandler {
break
}
let temperature = 0
let topP: number | undefined = undefined
if (this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning") {
// Recommended values from DeepSeek
temperature = 0.7
topP = 0.95
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
@@ -109,10 +121,13 @@ export class OpenRouterHandler implements ApiHandler {
const stream = await this.client.chat.completions.create({
model: model.id,
max_tokens: maxTokens,
temperature: 0,
temperature: temperature,
top_p: topP,
messages: openAiMessages,
stream: true,
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
})
let genId: string | undefined
@@ -136,6 +151,37 @@ export class OpenRouterHandler implements ApiHandler {
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// console.log("reasoning", delta.reasoning)
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
// if (didStreamThinkTagInReasoning) {
// yield {
// type: "text",
// // @ts-ignore-next-line
// text: delta.reasoning,
// }
// } else {
// yield {
// type: "reasoning",
// // @ts-ignore-next-line
// text: delta.reasoning,
// }
// // @ts-ignore-next-line
// reasoningResponse += delta.reasoning
// if (reasoningResponse.includes("</think>")) {
// didStreamThinkTagInReasoning = true
// console.log("did hit think tag", reasoningResponse)
// }
// }
}
// if (chunk.usage) {
// yield {
// type: "usage",
@@ -145,8 +191,31 @@ export class OpenRouterHandler implements ApiHandler {
// }
}
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
if (genId) {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
try {
const generationIterator = this.fetchGenerationDetails(genId)
const generation = (await generationIterator.next()).value
// console.log("OpenRouter generation details:", generation)
yield {
type: "usage",
// cacheWriteTokens: 0,
// cacheReadTokens: 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} catch (error) {
// ignore if fails
console.error("Error fetching OpenRouter generation details:", error)
}
}
}
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
async *fetchGenerationDetails(genId: string) {
// console.log("Fetching generation details for:", genId)
try {
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
headers: {
@@ -154,21 +223,11 @@ export class OpenRouterHandler implements ApiHandler {
},
timeout: 5_000, // this request hangs sometimes
})
const generation = response.data?.data
console.log("OpenRouter generation details:", response.data)
yield {
type: "usage",
// cacheWriteTokens: 0,
// cacheReadTokens: 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
yield response.data?.data
} catch (error) {
// ignore if fails
console.error("Error fetching OpenRouter generation details:", error)
throw error
}
}
@@ -178,9 +237,6 @@ export class OpenRouterHandler implements ApiHandler {
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
return {
id: openRouterDefaultModelId,
info: openRouterDefaultModelInfo,
}
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
}
}
+84
View File
@@ -0,0 +1,84 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
getModel(): { id: QwenModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in qwenModels) {
const id = modelId as QwenModelId
return { id, info: qwenModels[id] }
}
return {
id: qwenDefaultModelId,
info: qwenModels[qwenDefaultModelId],
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
}
}
+90
View File
@@ -0,0 +1,90 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.requestyModelId ?? ""
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
// Requesty usage includes an extra field for Anthropic use cases.
// Safely cast the prompt token details section to the appropriate structure.
interface RequestyUsage extends OpenAI.CompletionUsage {
prompt_tokens_details?: {
caching_tokens?: number
cached_tokens?: number
}
total_cost?: number
}
if (chunk.usage) {
const usage = chunk.usage as RequestyUsage
yield {
type: "usage",
inputTokens: usage.prompt_tokens || 0,
outputTokens: usage.completion_tokens || 0,
cacheWriteTokens: usage.prompt_tokens_details?.caching_tokens || undefined,
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
totalCost: usage.total_cost || undefined,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.requestyModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.togetherModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
}
}
+2
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
@@ -18,6 +19,7 @@ export class VertexHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.messages.create({
model: this.getModel().id,
+216
View File
@@ -0,0 +1,216 @@
import { describe, it } from "mocha"
import "should"
import { withRetry } from "./retry"
describe("Retry Decorator", () => {
describe("withRetry", () => {
it("should not retry on success", async () => {
let callCount = 0
class TestClass {
@withRetry()
async *successMethod() {
callCount++
yield "success"
}
}
const test = new TestClass()
const result = []
for await (const value of test.successMethod()) {
result.push(value)
}
callCount.should.equal(1)
result.should.deepEqual(["success"])
})
it("should retry on rate limit (429) error", async () => {
let callCount = 0
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should not retry on non-rate-limit errors", async () => {
let callCount = 0
class TestClass {
@withRetry()
async *failMethod() {
callCount++
throw new Error("Regular error")
}
}
const test = new TestClass()
try {
for await (const _ of test.failMethod()) {
// Should not reach here
}
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Regular error")
callCount.should.equal(1)
}
})
it("should respect retry-after header with delta seconds", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
error.headers = { "retry-after": "0.01" } // 10ms delay
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
duration.should.be.approximately(10, 10) // Allow 10ms variance
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should respect retry-after header with Unix timestamp", async () => {
let callCount = 0
const startTime = Date.now()
const retryTimestamp = Math.floor(Date.now() / 1000) + 0.01 // 10ms in the future
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
error.headers = { "retry-after": retryTimestamp.toString() }
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
duration.should.be.approximately(10, 10) // Allow 10ms variance
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should use exponential backoff when no retry-after header", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
// First retry should be after baseDelay (10ms)
duration.should.be.approximately(10, 10)
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should respect maxDelay", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 3, baseDelay: 50, maxDelay: 10 })
async *failMethod() {
callCount++
if (callCount < 3) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retries"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
// Both retries should be capped at maxDelay (10ms each)
duration.should.be.approximately(20, 20)
callCount.should.equal(3)
result.should.deepEqual(["success after retries"])
})
it("should throw after maxRetries attempts", async () => {
let callCount = 0
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10 })
async *failMethod() {
callCount++
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
}
const test = new TestClass()
try {
for await (const _ of test.failMethod()) {
// Should not reach here
}
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Rate limit exceeded")
callCount.should.equal(2) // Initial attempt + 1 retry
}
})
})
})
+64
View File
@@ -0,0 +1,64 @@
interface RetryOptions {
maxRetries?: number
baseDelay?: number
maxDelay?: number
retryAllErrors?: boolean
}
const DEFAULT_OPTIONS: Required<RetryOptions> = {
maxRetries: 3,
baseDelay: 1_000,
maxDelay: 10_000,
retryAllErrors: false,
}
export function withRetry(options: RetryOptions = {}) {
const { maxRetries, baseDelay, maxDelay, retryAllErrors } = { ...DEFAULT_OPTIONS, ...options }
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = async function* (...args: any[]) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
yield* originalMethod.apply(this, args)
return
} catch (error: any) {
const isRateLimit = error?.status === 429
const isLastAttempt = attempt === maxRetries - 1
if ((!isRateLimit && !retryAllErrors) || isLastAttempt) {
throw error
}
// Get retry delay from header or calculate exponential backoff
// Check various rate limit headers
const retryAfter =
error.headers?.["retry-after"] ||
error.headers?.["x-ratelimit-reset"] ||
error.headers?.["ratelimit-reset"]
let delay: number
if (retryAfter) {
// Handle both delta-seconds and Unix timestamp formats
const retryValue = parseInt(retryAfter, 10)
if (retryValue > Date.now() / 1000) {
// Unix timestamp
delay = retryValue * 1000 - Date.now()
} else {
// Delta seconds
delay = retryValue * 1000
}
} else {
// Use exponential backoff if no header
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
return descriptor
}
}
+92
View File
@@ -0,0 +1,92 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
/**
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
* This is required for DeepSeek Reasoner which does not support successive messages with the same role.
* DeepSeek highly recommends using 'user' role instead of 'system' role for optimal performance.
*
* @param messages Array of Anthropic messages
* @returns Array of OpenAI messages where consecutive messages with the same role are merged together
*/
export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]): OpenAI.Chat.ChatCompletionMessageParam[] {
return messages.reduce<OpenAI.Chat.ChatCompletionMessageParam[]>((merged, message) => {
const lastMessage = merged[merged.length - 1]
let messageContent: string | (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] =
""
let hasImages = false
if (Array.isArray(message.content)) {
const textParts: string[] = []
const imageParts: OpenAI.Chat.ChatCompletionContentPartImage[] = []
message.content.forEach((part) => {
if (part.type === "text") {
textParts.push(part.text)
}
if (part.type === "image") {
hasImages = true
imageParts.push({
type: "image_url",
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
})
}
})
if (hasImages) {
const parts: (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] = []
if (textParts.length > 0) {
parts.push({ type: "text", text: textParts.join("\n") })
}
parts.push(...imageParts)
messageContent = parts
} else {
messageContent = textParts.join("\n")
}
} else {
messageContent = message.content
}
// If the last message has the same role, merge the content
if (lastMessage?.role === message.role) {
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
lastMessage.content += `\n${messageContent}`
} else {
const lastContent = Array.isArray(lastMessage.content)
? lastMessage.content
: [{ type: "text" as const, text: lastMessage.content || "" }]
const newContent = Array.isArray(messageContent)
? messageContent
: [{ type: "text" as const, text: messageContent }]
if (message.role === "assistant") {
const mergedContent = [
...lastContent,
...newContent,
] as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"]
lastMessage.content = mergedContent
} else {
const mergedContent = [...lastContent, ...newContent] as OpenAI.Chat.ChatCompletionUserMessageParam["content"]
lastMessage.content = mergedContent
}
}
} else {
// Adds new message with the correct type based on role
if (message.role === "assistant") {
const newMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
role: "assistant",
content: messageContent as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"],
}
merged.push(newMessage)
} else {
const newMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
role: "user",
content: messageContent as OpenAI.Chat.ChatCompletionUserMessageParam["content"],
}
merged.push(newMessage)
}
}
return merged
}, [])
}
+6 -1
View File
@@ -1,11 +1,16 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk>
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamReasoningChunk | ApiStreamUsageChunk
export interface ApiStreamTextChunk {
type: "text"
text: string
}
export interface ApiStreamReasoningChunk {
type: "reasoning"
reasoning: string
}
export interface ApiStreamUsageChunk {
type: "usage"
inputTokens: number
+338 -163
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
import { ClineIgnoreController } from "./ClineIgnoreController"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { after, beforeEach, describe, it } from "mocha"
import "should"
describe("ClineIgnoreController", () => {
let tempDir: string
let controller: ClineIgnoreController
beforeEach(async () => {
// Create a temp directory for testing
tempDir = path.join(os.tmpdir(), `llm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
await fs.mkdir(tempDir)
// Create default .clineignore file
await fs.writeFile(
path.join(tempDir, ".clineignore"),
[".env", "*.secret", "private/", "# This is a comment", "", "temp.*", "file-with-space-at-end.* ", "**/.git/**"].join(
"\n",
),
)
controller = new ClineIgnoreController(tempDir)
await controller.initialize()
})
after(async () => {
// Clean up temp directory
await fs.rm(tempDir, { recursive: true, force: true })
})
describe("Default Patterns", () => {
// it("should block access to common ignored files", async () => {
// const results = [
// controller.validateAccess(".env"),
// controller.validateAccess(".git/config"),
// controller.validateAccess("node_modules/package.json"),
// ]
// results.forEach((result) => result.should.be.false())
// })
it("should allow access to regular files", async () => {
const results = [
controller.validateAccess("src/index.ts"),
controller.validateAccess("README.md"),
controller.validateAccess("package.json"),
]
results.forEach((result) => result.should.be.true())
})
it("should block access to .clineignore file", async () => {
const result = controller.validateAccess(".clineignore")
result.should.be.false()
})
})
describe("Custom Patterns", () => {
it("should block access to custom ignored patterns", async () => {
const results = [
controller.validateAccess("config.secret"),
controller.validateAccess("private/data.txt"),
controller.validateAccess("temp.json"),
controller.validateAccess("nested/deep/file.secret"),
controller.validateAccess("private/nested/deep/file.txt"),
]
results.forEach((result) => result.should.be.false())
})
it("should allow access to non-ignored files", async () => {
const results = [
controller.validateAccess("public/data.txt"),
controller.validateAccess("config.json"),
controller.validateAccess("src/temp/file.ts"),
controller.validateAccess("nested/deep/file.txt"),
controller.validateAccess("not-private/data.txt"),
]
results.forEach((result) => result.should.be.true())
})
it("should handle pattern edge cases", async () => {
await fs.writeFile(
path.join(tempDir, ".clineignore"),
["*.secret", "private/", "*.tmp", "data-*.json", "temp/*"].join("\n"),
)
controller = new ClineIgnoreController(tempDir)
await controller.initialize()
const results = [
controller.validateAccess("data-123.json"), // Should be false (wildcard)
controller.validateAccess("data.json"), // Should be true (doesn't match pattern)
controller.validateAccess("script.tmp"), // Should be false (extension match)
]
results[0].should.be.false() // data-123.json
results[1].should.be.true() // data.json
results[2].should.be.false() // script.tmp
})
// ToDo: handle negation patterns successfully
// it("should handle negation patterns", async () => {
// await fs.writeFile(
// path.join(tempDir, ".clineignore"),
// [
// "temp/*", // Ignore everything in temp
// "!temp/allowed/*", // But allow files in temp/allowed
// "docs/**/*.md", // Ignore all markdown files in docs
// "!docs/README.md", // Except README.md
// "!docs/CONTRIBUTING.md", // And CONTRIBUTING.md
// "assets/", // Ignore all assets
// "!assets/public/", // Except public assets
// "!assets/public/*.png", // Specifically allow PNGs in public assets
// ].join("\n"),
// )
// controller = new ClineIgnoreController(tempDir)
// const results = [
// // Basic negation
// controller.validateAccess("temp/file.txt"), // Should be false (in temp/)
// controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated)
// controller.validateAccess("temp/allowed/nested/file.txt"), // Should be true (negated with nested)
// // Multiple negations in same path
// controller.validateAccess("docs/guide.md"), // Should be false (matches docs/**/*.md)
// controller.validateAccess("docs/README.md"), // Should be true (negated)
// controller.validateAccess("docs/CONTRIBUTING.md"), // Should be true (negated)
// controller.validateAccess("docs/api/guide.md"), // Should be false (nested markdown)
// // Nested negations
// controller.validateAccess("assets/logo.png"), // Should be false (in assets/)
// controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png)
// controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/)
// ]
// results[0].should.be.false() // temp/file.txt
// results[1].should.be.true() // temp/allowed/file.txt
// results[2].should.be.true() // temp/allowed/nested/file.txt
// results[3].should.be.false() // docs/guide.md
// results[4].should.be.true() // docs/README.md
// results[5].should.be.true() // docs/CONTRIBUTING.md
// results[6].should.be.false() // docs/api/guide.md
// results[7].should.be.false() // assets/logo.png
// results[8].should.be.true() // assets/public/logo.png
// results[9].should.be.true() // assets/public/data.json
// })
it("should handle comments in .clineignore", async () => {
// Create a new .clineignore with comments
await fs.writeFile(
path.join(tempDir, ".clineignore"),
["# Comment line", "*.secret", "private/", "temp.*"].join("\n"),
)
controller = new ClineIgnoreController(tempDir)
await controller.initialize()
const result = controller.validateAccess("test.secret")
result.should.be.false()
})
})
describe("Path Handling", () => {
it("should handle absolute paths and match ignore patterns", async () => {
// Test absolute path that should be allowed
const allowedPath = path.join(tempDir, "src/file.ts")
const allowedResult = controller.validateAccess(allowedPath)
allowedResult.should.be.true()
// Test absolute path that matches an ignore pattern (*.secret)
const ignoredPath = path.join(tempDir, "config.secret")
const ignoredResult = controller.validateAccess(ignoredPath)
ignoredResult.should.be.false()
// Test absolute path in ignored directory (private/)
const ignoredDirPath = path.join(tempDir, "private/data.txt")
const ignoredDirResult = controller.validateAccess(ignoredDirPath)
ignoredDirResult.should.be.false()
})
it("should handle relative paths and match ignore patterns", async () => {
// Test relative path that should be allowed
const allowedResult = controller.validateAccess("./src/file.ts")
allowedResult.should.be.true()
// Test relative path that matches an ignore pattern (*.secret)
const ignoredResult = controller.validateAccess("./config.secret")
ignoredResult.should.be.false()
// Test relative path in ignored directory (private/)
const ignoredDirResult = controller.validateAccess("./private/data.txt")
ignoredDirResult.should.be.false()
})
it("should normalize paths with backslashes", async () => {
const result = controller.validateAccess("src\\file.ts")
result.should.be.true()
})
})
describe("Batch Filtering", () => {
it("should filter an array of paths", async () => {
const paths = ["src/index.ts", ".env", "lib/utils.ts", ".git/config", "dist/bundle.js"]
const filtered = controller.filterPaths(paths)
filtered.should.deepEqual(["src/index.ts", "lib/utils.ts", "dist/bundle.js"])
})
})
describe("Error Handling", () => {
it("should handle invalid paths", async () => {
// Test with an invalid path containing null byte
const result = controller.validateAccess("\0invalid")
result.should.be.true()
})
it("should handle missing .clineignore gracefully", async () => {
// Create a new controller in a directory without .clineignore
const emptyDir = path.join(os.tmpdir(), `llm-test-empty-${Date.now()}`)
await fs.mkdir(emptyDir)
try {
const controller = new ClineIgnoreController(emptyDir)
await controller.initialize()
const result = controller.validateAccess("file.txt")
result.should.be.true()
} finally {
await fs.rm(emptyDir, { recursive: true, force: true })
}
})
it("should handle empty .clineignore", async () => {
await fs.writeFile(path.join(tempDir, ".clineignore"), "")
controller = new ClineIgnoreController(tempDir)
await controller.initialize()
const result = controller.validateAccess("regular-file.txt")
result.should.be.true()
})
})
})
+189
View File
@@ -0,0 +1,189 @@
import path from "path"
import { fileExistsAtPath } from "../../utils/fs"
import fs from "fs/promises"
import ignore, { Ignore } from "ignore"
import * as vscode from "vscode"
export const LOCK_TEXT_SYMBOL = "\u{1F512}"
/**
* Controls LLM access to files by enforcing ignore patterns.
* Designed to be instantiated once in Cline.ts and passed to file manipulation services.
* Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files.
*/
export class ClineIgnoreController {
private cwd: string
private ignoreInstance: Ignore
private disposables: vscode.Disposable[] = []
clineIgnoreContent: string | undefined
constructor(cwd: string) {
this.cwd = cwd
this.ignoreInstance = ignore()
this.clineIgnoreContent = undefined
// Set up file watcher for .clineignore
this.setupFileWatcher()
}
/**
* Initialize the controller by loading custom patterns
* Must be called after construction and before using the controller
*/
async initialize(): Promise<void> {
await this.loadClineIgnore()
}
/**
* Set up the file watcher for .clineignore changes
*/
private setupFileWatcher(): void {
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
const fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
// Watch for changes and updates
this.disposables.push(
fileWatcher.onDidChange(() => {
this.loadClineIgnore()
}),
fileWatcher.onDidCreate(() => {
this.loadClineIgnore()
}),
fileWatcher.onDidDelete(() => {
this.loadClineIgnore()
}),
)
// Add fileWatcher itself to disposables
this.disposables.push(fileWatcher)
}
/**
* Load custom patterns from .clineignore if it exists
*/
private async loadClineIgnore(): Promise<void> {
try {
// Reset ignore instance to prevent duplicate patterns
this.ignoreInstance = ignore()
const ignorePath = path.join(this.cwd, ".clineignore")
if (await fileExistsAtPath(ignorePath)) {
const content = await fs.readFile(ignorePath, "utf8")
this.clineIgnoreContent = content
this.ignoreInstance.add(content)
this.ignoreInstance.add(".clineignore")
} else {
this.clineIgnoreContent = undefined
}
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .clineignore:", error)
}
}
/**
* Check if a file should be accessible to the LLM
* @param filePath - Path to check (relative to cwd)
* @returns true if file is accessible, false if ignored
*/
validateAccess(filePath: string): boolean {
// Always allow access if .clineignore does not exist
if (!this.clineIgnoreContent) {
return true
}
try {
// Normalize path to be relative to cwd and use forward slashes
const absolutePath = path.resolve(this.cwd, filePath)
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
// Ignore expects paths to be path.relative()'d
return !this.ignoreInstance.ignores(relativePath)
} catch (error) {
// console.error(`Error validating access for ${filePath}:`, error)
// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd.
return true
}
}
/**
* Check if a terminal command should be allowed to execute based on file access patterns
* @param command - Terminal command to validate
* @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
*/
validateCommand(command: string): string | undefined {
// Always allow if no .clineignore exists
if (!this.clineIgnoreContent) {
return undefined
}
// Split command into parts and get the base command
const parts = command.trim().split(/\s+/)
const baseCommand = parts[0].toLowerCase()
// Commands that read file contents
const fileReadingCommands = [
// Unix commands
"cat",
"less",
"more",
"head",
"tail",
"grep",
"awk",
"sed",
// PowerShell commands and aliases
"get-content",
"gc",
"type",
"select-string",
"sls",
]
if (fileReadingCommands.includes(baseCommand)) {
// Check each argument that could be a file path
for (let i = 1; i < parts.length; i++) {
const arg = parts[i]
// Skip command flags/options (both Unix and PowerShell style)
if (arg.startsWith("-") || arg.startsWith("/")) {
continue
}
// Ignore PowerShell parameter names
if (arg.includes(":")) {
continue
}
// Validate file access
if (!this.validateAccess(arg)) {
return arg
}
}
}
return undefined
}
/**
* Filter an array of paths, removing those that should be ignored
* @param paths - Array of paths to filter (relative to cwd)
* @returns Array of allowed paths
*/
filterPaths(paths: string[]): string[] {
try {
return paths
.map((p) => ({
path: p,
allowed: this.validateAccess(p),
}))
.filter((x) => x.allowed)
.map((x) => x.path)
} catch (error) {
console.error("Error filtering paths:", error)
return [] // Fail closed for security
}
}
/**
* Clean up resources when the controller is no longer needed
*/
dispose(): void {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
}
}
+37 -5
View File
@@ -7,27 +7,32 @@ import fs from "fs/promises"
import { extractTextFromFile } from "../../integrations/misc/extract-text"
import { isBinaryFile } from "isbinaryfile"
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
import { getCommitInfo } from "../../utils/git"
import { getWorkingState } from "../../utils/git"
export function openMention(mention?: string): void {
if (!mention) {
return
}
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return
}
if (mention.startsWith("/")) {
const relPath = mention.slice(1)
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return
}
const absPath = path.resolve(cwd, relPath)
if (mention.endsWith("/")) {
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
} else {
openFile(absPath)
}
} else if (mention === "problems") {
vscode.commands.executeCommand("workbench.actions.view.problems")
} else if (mention === "terminal") {
vscode.commands.executeCommand("workbench.action.terminal.focus")
} else if (mention.startsWith("http")) {
vscode.env.openExternal(vscode.Uri.parse(mention))
}
@@ -46,6 +51,12 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
: `'${mentionPath}' (see below for file content)`
} else if (mention === "problems") {
return `Workspace Problems (see below for diagnostics)`
} else if (mention === "terminal") {
return `Terminal Output (see below for output)`
} else if (mention === "git-changes") {
return `Working directory changes (see below for details)`
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
return `Git commit '${mention}' (see below for commit info)`
}
return match
})
@@ -99,6 +110,27 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
} catch (error) {
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
}
} else if (mention === "terminal") {
try {
const terminalOutput = await getLatestTerminalOutput()
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
} catch (error) {
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
}
} else if (mention === "git-changes") {
try {
const workingState = await getWorkingState(cwd)
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
} catch (error) {
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
}
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
try {
const commitInfo = await getCommitInfo(mention, cwd)
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
} catch (error) {
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
}
}
}
+30 -8
View File
@@ -1,15 +1,16 @@
import { Anthropic } from "@anthropic-ai/sdk"
import * as path from "path"
import * as diff from "diff"
import * as path from "path"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
export const formatResponse = {
toolDenied: () => `The user denied this operation.`,
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
clineIgnoreError: (path: string) =>
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
noToolsUsed: () =>
`[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
@@ -46,7 +47,12 @@ Otherwise, if you have not completed the task and do not need additional informa
return formatImagesIntoBlocks(images)
},
formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => {
formatFilesList: (
absolutePath: string,
files: string[],
didHitLimit: boolean,
clineIgnoreController?: ClineIgnoreController,
): string => {
const sorted = files
.map((file) => {
// convert absolute path to relative path
@@ -77,14 +83,30 @@ Otherwise, if you have not completed the task and do not need additional informa
// the shorter one comes first
return aParts.length - bParts.length
})
const clineIgnoreParsed = clineIgnoreController
? sorted.map((filePath) => {
// path is relative to absolute path, not cwd
// validateAccess expects either path relative to cwd or absolute path
// otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored.
const absoluteFilePath = path.resolve(absolutePath, filePath)
const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath)
if (isIgnored) {
return LOCK_TEXT_SYMBOL + " " + filePath
}
return filePath
})
: sorted
if (didHitLimit) {
return `${sorted.join(
return `${clineIgnoreParsed.join(
"\n",
)}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)`
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
} else if (clineIgnoreParsed.length === 0 || (clineIgnoreParsed.length === 1 && clineIgnoreParsed[0] === "")) {
return "No files found."
} else {
return sorted.join("\n")
return clineIgnoreParsed.join("\n")
}
},
+34 -9
View File
@@ -1,4 +1,4 @@
import defaultShell from "default-shell"
import { getShell } from "../../utils/shell"
import os from "os"
import osName from "os-name"
import { McpHub } from "../../services/mcp/McpHub"
@@ -38,7 +38,7 @@ Always adhere to this format for the tool use to ensure proper parsing and execu
# Tools
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Parameters:
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
- requires_approval: (required) A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations.
@@ -331,7 +331,24 @@ ${
<access_mcp_resource>
<server_name>weather-server</server_name>
<uri>weather://san-francisco/current</uri>
</access_mcp_resource>`
</access_mcp_resource>
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
<use_mcp_tool>
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
<tool_name>create_issue</tool_name>
<arguments>
{
"owner": "octocat",
"repo": "hello-world",
"title": "Found a bug",
"body": "I'm having a problem with this.",
"labels": ["bug", "help wanted"],
"assignees": ["octocat"]
}
</arguments>
</use_mcp_tool>`
: ""
}
@@ -750,7 +767,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
@@ -865,9 +882,10 @@ In each user message, the environment_details will specify the current mode. The
## What is PLAN MODE?
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
====
@@ -941,7 +959,7 @@ ${
SYSTEM INFORMATION
Operating System: ${osName()}
Default Shell: ${defaultShell}
Default Shell: ${getShell()}
Home Directory: ${os.homedir().toPosix()}
Current Working Directory: ${cwd.toPosix()}
@@ -957,13 +975,20 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) {
export function addUserInstructions(
settingsCustomInstructions?: string,
clineRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
) {
let customInstructions = ""
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (clineRulesFileInstructions) {
customInstructions += clineRulesFileInstructions
customInstructions += clineRulesFileInstructions + "\n\n"
}
if (clineIgnoreInstructions) {
customInstructions += clineIgnoreInstructions
}
return `
+1 -1
View File
@@ -73,7 +73,7 @@ export function getNextTruncationRange(
let rangeEndIndex = startOfRest + messagesToRemove - 1
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
// NOTE: anthropic format messages are always user-assitant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
if (messages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}
+482 -98
View File
@@ -14,10 +14,11 @@ import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp"
import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
import { ExtensionMessage, ExtensionState, Platform } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
import { fileExistsAtPath } from "../../utils/fs"
@@ -28,6 +29,9 @@ import { getUri } from "./getUri"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
import { searchCommits } from "../../utils/git"
import { ChatContent } from "../../shared/ChatContent"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -45,7 +49,11 @@ type SecretKey =
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "qwenApiKey"
| "mistralApiKey"
| "liteLlmApiKey"
| "authToken"
| "authNonce"
type GlobalStateKey =
@@ -53,6 +61,8 @@ type GlobalStateKey =
| "apiModelId"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
@@ -60,6 +70,7 @@ type GlobalStateKey =
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "ollamaBaseUrl"
| "lmStudioModelId"
@@ -76,6 +87,12 @@ type GlobalStateKey =
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "qwenApiLine"
| "requestyModelId"
| "togetherModelId"
| "mcpMarketplaceCatalog"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -92,10 +109,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private disposables: vscode.Disposable[] = []
private view?: vscode.WebviewView | vscode.WebviewPanel
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
private authManager: FirebaseAuthManager
private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -174,7 +191,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webviewView.webview.html = this.getHtmlContent(webviewView.webview)
// Sets up an event listener to listen for messages passed from the webview view context
// and executes code based on the message that is recieved
// and executes code based on the message that is received
this.setWebviewMessageListener(webviewView.webview)
// Logs show up in bottom panel > Debug Console
@@ -223,7 +240,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.disposables,
)
// Listen for when color changes
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
@@ -233,6 +250,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.postStateToWebview()
}
},
null,
this.disposables,
@@ -245,7 +266,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await this.getState()
this.cline = new Cline(
@@ -324,15 +345,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -359,7 +380,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
/**
* Sets up an event listener to listen for messages passed from the webview context and
* executes code based on the message that is recieved.
* executes code based on the message that is received.
*
* @param webview A reference to the extension webview
*/
@@ -369,7 +390,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
switch (message.type) {
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.initializeFilePaths() // don't await
this.workspaceTracker?.populateFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({
type: "theme",
@@ -388,6 +409,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
this.getGlobalState("mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
this.refreshOpenRouterModels().then(async (openRouterModels) => {
if (openRouterModels) {
// 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)
@@ -401,6 +433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
})
break
case "newTask":
// Code that should run in response to the hello message command
@@ -425,11 +458,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
lmStudioModelId,
@@ -438,11 +474,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
qwenApiLine,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@@ -453,11 +498,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("awsProfile", awsProfile)
await this.updateGlobalState("awsUseProfile", awsUseProfile)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
@@ -466,11 +514,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.storeSecret("requestyApiKey", requestyApiKey)
await this.storeSecret("togetherApiKey", togetherApiKey)
await this.storeSecret("qwenApiKey", qwenApiKey)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.storeSecret("liteLlmApiKey", liteLlmApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl)
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
await this.updateGlobalState("requestyModelId", requestyModelId)
await this.updateGlobalState("togetherModelId", togetherModelId)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@@ -498,90 +555,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
}
break
case "chatSettings":
case "togglePlanActMode":
if (message.chatSettings) {
const didSwitchToActMode = message.chatSettings.mode === "act"
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
}
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
}
}
await this.updateGlobalState("chatSettings", message.chatSettings)
await this.postStateToWebview()
if (this.cline) {
this.cline.updateChatSettings(message.chatSettings)
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
this.cline.didRespondToPlanAskBySwitchingMode = true
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: "[Proceeding with the task...]",
})
} else {
this.cancelTask()
}
}
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
}
break
// case "relaunchChromeDebugMode":
@@ -721,6 +697,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.handleSignOut()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
break
}
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
@@ -728,6 +708,69 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "fetchMcpMarketplace": {
await this.fetchMcpMarketplace(message.bool)
break
}
case "downloadMcp": {
if (message.mcpId) {
// 1. Toggle to act mode if we are in plan mode
const { chatSettings } = await this.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await this.togglePlanActModeWithChatSettings({ mode: "act" })
}
// 2. Enable MCP settings if disabled
// Enable MCP mode if disabled
const mcpConfig = vscode.workspace.getConfiguration("cline.mcp")
if (mcpConfig.get<string>("mode") !== "full") {
await mcpConfig.update("mode", "full", true)
}
// 3. download MCP
await this.downloadMcp(message.mcpId)
}
break
}
case "silentlyRefreshMcpMarketplace": {
await this.silentlyRefreshMcpMarketplace()
break
}
// case "openMcpMarketplaceServerDetails": {
// if (message.text) {
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
// const details: McpDownloadResponse = await response.json()
// if (details.readmeContent) {
// // Disable markdown preview markers
// const config = vscode.workspace.getConfiguration("markdown")
// await config.update("preview.markEditorSelection", false, true)
// // Create URI with base64 encoded markdown content
// const uri = vscode.Uri.parse(
// `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`,
// )
// // close existing
// const tabs = vscode.window.tabGroups.all
// .flatMap((tg) => tg.tabs)
// .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview"))
// for (const tab of tabs) {
// await vscode.window.tabGroups.close(tab)
// }
// // Show only the preview
// await vscode.commands.executeCommand("markdown.showPreview", uri, {
// sideBySide: true,
// preserveFocus: true,
// })
// }
// }
// this.postMessageToWebview({ type: "relinquishControl" })
// break
// }
case "toggleMcpServer": {
try {
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
@@ -752,6 +795,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "deleteMcpServer": {
if (message.serverName) {
this.mcpHub?.deleteServer(message.serverName)
}
break
}
case "fetchLatestMcpServersFromHub": {
this.mcpHub?.sendLatestMcpServers()
break
}
case "searchCommits": {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (cwd) {
try {
const commits = await searchCommits(message.text || "", cwd)
await this.postMessageToWebview({
type: "commitSearchResults",
commits,
})
} catch (error) {
console.error(`Error searching commits: ${JSON.stringify(error)}`)
}
}
break
}
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
@@ -769,6 +837,106 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent) {
const didSwitchToActMode = chatSettings.mode === "act"
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
}
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
await this.updateGlobalState("openAiModelInfo", newModelInfo)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
}
if (this.cline) {
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
this.cline.api = buildApiHandler(updatedApiConfiguration)
}
}
await this.updateGlobalState("chatSettings", chatSettings)
await this.postStateToWebview()
// console.log("chatSettings", message.chatSettings)
if (this.cline) {
this.cline.updateChatSettings(chatSettings)
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
this.cline.didRespondToPlanAskBySwitchingMode = true
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
images: chatContent?.images,
})
} else {
this.cancelTask()
}
}
}
async subscribeEmail(email?: string) {
if (!email) {
return
@@ -951,6 +1119,171 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
headers: {
"Content-Type": "application/json",
},
})
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
const catalog: McpMarketplaceCatalog = {
items: (response.data || []).map((item: any) => ({
...item,
githubStars: item.githubStars ?? 0,
downloadCount: item.downloadCount ?? 0,
tags: item.tags ?? [],
})),
}
// Store in global state
await this.updateGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
}
}
async silentlyRefreshMcpMarketplace() {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
}
}
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
try {
// Check if we have cached data
const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as McpMarketplaceCatalog | undefined
if (!forceRefresh && cachedCatalog?.items) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
private async downloadMcp(mcpId: string) {
try {
// First check if we already have this MCP server installed
const servers = this.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await this.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README
const task = `Set up the MCP server from ${mcpDetails.githubUrl}.
Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
// Initialize task and show chat view
await this.initClineWithTask(task)
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await this.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
}
}
// OpenAi
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
@@ -1252,6 +1585,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
chatSettings,
userInfo,
authToken,
mcpMarketplaceEnabled,
} = await this.getState()
return {
@@ -1264,11 +1598,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
platform: process.platform as Platform,
autoApprovalSettings,
browserSettings,
chatSettings,
isLoggedIn: !!authToken,
userInfo,
mcpMarketplaceEnabled,
}
}
@@ -1283,7 +1619,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
Now that we use retainContextWhenHidden, we don't have to store a cache of cline messages in the user's state, but we could to reduce memory footprint in long conversations.
- We have to be careful of what state is shared between ClineProvider instances since there could be multiple instances of the extension running at once. For example when we cached cline messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notify the other instances that the API key has changed.
We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels.
@@ -1334,11 +1670,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
lmStudioModelId,
@@ -1347,6 +1686,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
@@ -1358,11 +1702,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
qwenApiLine,
liteLlmApiKey,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1373,11 +1721,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("awsSessionToken") as Promise<string | undefined>,
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
this.getGlobalState("awsProfile") as Promise<string | undefined>,
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
this.getSecret("openAiApiKey") as Promise<string | undefined>,
this.getGlobalState("openAiModelId") as Promise<string | undefined>,
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
@@ -1386,6 +1737,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("requestyApiKey") as Promise<string | undefined>,
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
this.getSecret("togetherApiKey") as Promise<string | undefined>,
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
@@ -1397,11 +1753,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("browserSettings") as Promise<BrowserSettings | undefined>,
this.getGlobalState("chatSettings") as Promise<ChatSettings | undefined>,
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("liteLlmBaseUrl") as Promise<string | undefined>,
this.getGlobalState("liteLlmModelId") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@@ -1418,6 +1778,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
const o3MiniReasoningEffort = vscode.workspace
.getConfiguration("cline.modelSettings.o3Mini")
.get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
return {
apiConfiguration: {
apiProvider,
@@ -1429,11 +1795,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
lmStudioModelId,
@@ -1442,11 +1811,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
},
lastShownAnnouncementId,
customInstructions,
@@ -1459,6 +1838,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
mcpMarketplaceEnabled,
}
}
@@ -1535,7 +1915,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"qwenApiKey",
"mistralApiKey",
"liteLlmApiKey",
"authToken",
]
for (const key of secretKeys) {
@@ -0,0 +1,45 @@
import * as vscode from "vscode"
/**
* Gets the contents of the active terminal
* @returns The terminal contents as a string
*/
export async function getLatestTerminalOutput(): Promise<string> {
// Store original clipboard content to restore later
const originalClipboard = await vscode.env.clipboard.readText()
try {
// Select terminal content
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
// Copy selection to clipboard
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
// Clear the selection
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
// Get terminal contents from clipboard
let terminalContents = (await vscode.env.clipboard.readText()).trim()
// Check if there's actually a terminal open
if (terminalContents === originalClipboard) {
return ""
}
// Clean up command separation
const lines = terminalContents.split("\n")
const lastLine = lines.pop()?.trim()
if (lastLine) {
let i = lines.length - 1
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
i--
}
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
}
return terminalContents
} finally {
// Restore original clipboard content
await vscode.env.clipboard.writeText(originalClipboard)
}
}
@@ -16,7 +16,7 @@ class WorkspaceTracker {
this.registerListeners()
}
async initializeFilePaths() {
async populateFilePaths() {
// should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file
if (!cwd) {
return
+9 -5
View File
@@ -42,11 +42,15 @@ export class BrowserSession {
await fs.mkdir(puppeteerDir, { recursive: true })
}
// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
// if it does exist it will return the path to existing chromium
const stats: PCRStats = await PCR({
downloadPath: puppeteerDir,
})
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
}
const stats: PCRStats = chromeExecutablePath
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
// if it does exist it will return the path to existing chromium
await PCR({ downloadPath: puppeteerDir })
return stats
}
+4 -2
View File
@@ -45,9 +45,11 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
onlyFiles: false, // true by default, false means it will list directories on their own too
}
// * globs all files in one dir, ** globs files in nested directories
const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
return [files, files.length >= limit]
const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
return [filePaths, filePaths.length >= limit]
}
/*
+31
View File
@@ -472,6 +472,10 @@ export class McpHub {
})
}
async sendLatestMcpServers() {
await this.notifyWebviewOfServerChanges()
}
// Using server
// Public methods for server management
@@ -632,6 +636,33 @@ export class McpHub {
}
}
public async deleteServer(serverName: string) {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
if (config.mcpServers[serverName]) {
delete config.mcpServers[serverName]
const updatedConfig = {
mcpServers: config.mcpServers,
}
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
await this.updateServerConnections(config.mcpServers)
vscode.window.showInformationMessage(`Deleted ${serverName} MCP server`)
} else {
vscode.window.showWarningMessage(`${serverName} not found in MCP configuration`)
}
} catch (error) {
vscode.window.showErrorMessage(
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
)
throw error
}
}
async dispose(): Promise<void> {
this.removeAllFileWatchers()
for (const connection of this.connections) {
+19 -15
View File
@@ -1,8 +1,9 @@
import * as vscode from "vscode"
import * as childProcess from "child_process"
import * as path from "path"
import * as fs from "fs"
import * as readline from "readline"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
/*
This file provides functionality to perform regex searches on files using ripgrep.
@@ -50,7 +51,7 @@ const isWindows = /^win/.test(process.platform)
const binName = isWindows ? "rg.exe" : "rg"
interface SearchResult {
file: string
filePath: string
line: number
column: number
match: string
@@ -63,7 +64,7 @@ const MAX_RESULTS = 300
async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
const checkPath = async (pkgFolder: string) => {
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
return (await pathExists(fullPath)) ? fullPath : undefined
return (await fileExistsAtPath(fullPath)) ? fullPath : undefined
}
return (
@@ -74,14 +75,6 @@ async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
)
}
async function pathExists(path: string): Promise<boolean> {
return new Promise((resolve) => {
fs.access(path, (err) => {
resolve(err === null)
})
})
}
async function execRipgrep(bin: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
const rgProcess = childProcess.spawn(bin, args)
@@ -122,7 +115,13 @@ async function execRipgrep(bin: string, args: string[]): Promise<string> {
})
}
export async function regexSearchFiles(cwd: string, directoryPath: string, regex: string, filePattern?: string): Promise<string> {
export async function regexSearchFiles(
cwd: string,
directoryPath: string,
regex: string,
filePattern?: string,
clineIgnoreController?: ClineIgnoreController,
): Promise<string> {
const vscodeAppRoot = vscode.env.appRoot
const rgPath = await getBinPath(vscodeAppRoot)
@@ -150,7 +149,7 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex
results.push(currentResult as SearchResult)
}
currentResult = {
file: parsed.data.path.text,
filePath: parsed.data.path.text,
line: parsed.data.line_number,
column: parsed.data.submatches[0].start,
match: parsed.data.lines.text,
@@ -174,7 +173,12 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex
results.push(currentResult as SearchResult)
}
return formatResults(results, cwd)
// Filter results using ClineIgnoreController if provided
const filteredResults = clineIgnoreController
? results.filter((result) => clineIgnoreController.validateAccess(result.filePath))
: results
return formatResults(filteredResults, cwd)
}
function formatResults(results: SearchResult[], cwd: string): string {
@@ -189,7 +193,7 @@ function formatResults(results: SearchResult[], cwd: string): string {
// Group results by file name
results.slice(0, MAX_RESULTS).forEach((result) => {
const relativeFilePath = path.relative(cwd, result.file)
const relativeFilePath = path.relative(cwd, result.filePath)
if (!groupedResults[relativeFilePath]) {
groupedResults[relativeFilePath] = []
}
+21 -6
View File
@@ -3,9 +3,13 @@ import * as path from "path"
import { listFiles } from "../glob/list-files"
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
export async function parseSourceCodeForDefinitionsTopLevel(
dirPath: string,
clineIgnoreController?: ClineIgnoreController,
): Promise<string> {
// check if the path exists
const dirExists = await fileExistsAtPath(path.resolve(dirPath))
if (!dirExists) {
@@ -24,10 +28,14 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
// Parse specific files we have language parsers for
// const filesWithoutDefinitions: string[] = []
for (const file of filesToParse) {
const definitions = await parseFile(file, languageParsers)
// Filter filepaths for access if controller is provided
const allowedFilesToParse = clineIgnoreController ? clineIgnoreController.filterPaths(filesToParse) : filesToParse
for (const filePath of allowedFilesToParse) {
const definitions = await parseFile(filePath, languageParsers, clineIgnoreController)
if (definitions) {
result += `${path.relative(dirPath, file).toPosix()}\n${definitions}\n`
result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n`
}
// else {
// filesWithoutDefinitions.push(file)
@@ -98,7 +106,14 @@ This approach allows us to focus on the most relevant parts of the code (defined
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/helper.js
- https://tree-sitter.github.io/tree-sitter/code-navigation-systems
*/
async function parseFile(filePath: string, languageParsers: LanguageParser): Promise<string | undefined> {
async function parseFile(
filePath: string,
languageParsers: LanguageParser,
clineIgnoreController?: ClineIgnoreController,
): Promise<string | null> {
if (clineIgnoreController && !clineIgnoreController.validateAccess(filePath)) {
return null
}
const fileContent = await fs.readFile(filePath, "utf8")
const ext = path.extname(filePath).toLowerCase().slice(1)
@@ -159,5 +174,5 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro
if (formattedOutput.length > 0) {
return `|----\n${formattedOutput}|----\n`
}
return undefined
return null
}
+4
View File
@@ -0,0 +1,4 @@
export interface ChatContent {
message?: string
images?: string[]
}
+20 -1
View File
@@ -1,11 +1,12 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
import { GitCommit } from "../utils/git"
import { ApiConfiguration, ModelInfo } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
// webview will hold state
export interface ExtensionMessage {
@@ -26,6 +27,9 @@ export interface ExtensionMessage {
| "vsCodeLmModels"
| "requestVsCodeLmModels"
| "emailSubscribed"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
text?: string
action?:
| "chatButtonClicked"
@@ -46,8 +50,16 @@ export interface ExtensionMessage {
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
mcpServers?: McpServer[]
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpDownloadResponse
commits?: GitCommit[]
}
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
version: string
apiConfiguration?: ApiConfiguration
@@ -62,11 +74,13 @@ export interface ExtensionState {
browserSettings: BrowserSettings
chatSettings: ChatSettings
isLoggedIn: boolean
platform: Platform
userInfo?: {
displayName: string | null
email: string | null
photoURL: string | null
}
mcpMarketplaceEnabled?: boolean
}
export interface ClineMessage {
@@ -75,9 +89,11 @@ export interface ClineMessage {
ask?: ClineAsk
say?: ClineSay
text?: string
reasoning?: string
images?: string[]
partial?: boolean
lastCheckpointHash?: string
isCheckpointCheckedOut?: boolean
conversationHistoryIndex?: number
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
}
@@ -103,6 +119,7 @@ export type ClineSay =
| "api_req_started"
| "api_req_finished"
| "text"
| "reasoning"
| "completion_result"
| "user_feedback"
| "user_feedback_diff"
@@ -119,6 +136,8 @@ export type ClineSay =
| "use_mcp_server"
| "diff_error"
| "deleted_api_reqs"
| "clineignore_error"
| "checkpoint_created"
export interface ClineSayTool {
tool:
+11 -1
View File
@@ -2,6 +2,7 @@ import { ApiConfiguration } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { ChatContent } from "./ChatContent"
export interface WebviewMessage {
type:
@@ -28,9 +29,10 @@ export interface WebviewMessage {
| "refreshOpenAiModels"
| "openMcpSettings"
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "chatSettings"
| "togglePlanActMode"
| "checkpointDiff"
| "checkpointRestore"
| "taskCompletionViewChanges"
@@ -42,6 +44,12 @@ export interface WebviewMessage {
| "accountLoginClicked"
| "accountLogoutClicked"
| "subscribeEmail"
| "fetchMcpMarketplace"
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
| "searchCommits"
| "showMcpView"
| "fetchLatestMcpServersFromHub"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -53,6 +61,8 @@ export interface WebviewMessage {
autoApprovalSettings?: AutoApprovalSettings
browserSettings?: BrowserSettings
chatSettings?: ChatSettings
chatContent?: ChatContent
mcpId?: string
// For toggleToolAutoApprove
serverName?: string
+352 -19
View File
@@ -8,13 +8,20 @@ export type ApiProvider =
| "lmstudio"
| "gemini"
| "openai-native"
| "requesty"
| "together"
| "deepseek"
| "qwen"
| "mistral"
| "vscode-lm"
| "litellm"
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmApiKey?: string
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
@@ -24,11 +31,14 @@ export interface ApiHandlerOptions {
awsSessionToken?: string
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsUseProfile?: boolean
awsProfile?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
openAiModelInfo?: ModelInfo
ollamaModelId?: string
ollamaBaseUrl?: string
lmStudioModelId?: string
@@ -36,9 +46,16 @@ export interface ApiHandlerOptions {
geminiApiKey?: string
openAiNativeApiKey?: string
deepSeekApiKey?: string
requestyApiKey?: string
requestyModelId?: string
togetherApiKey?: string
togetherModelId?: string
qwenApiKey?: string
mistralApiKey?: string
azureApiVersion?: string
vsCodeLmModelSelector?: any
o3MiniReasoningEffort?: string
qwenApiLine?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -166,7 +183,7 @@ export const bedrockModels = {
// OpenRouter
// https://openrouter.ai/models?order=newest&supported_parameters=tools
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet" // will always exist in openRouterModels
export const openRouterDefaultModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
@@ -178,7 +195,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description:
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._",
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
}
// Vertex AI
@@ -241,10 +258,34 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219"
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiModels = {
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-lite-preview-02-05": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-thinking-exp-01-21": {
maxTokens: 65536,
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
@@ -267,14 +308,6 @@ export const geminiModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-002": {
maxTokens: 8192,
contextWindow: 1_048_576,
@@ -315,6 +348,14 @@ export const geminiModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
// OpenAI Native
@@ -322,6 +363,14 @@ export const geminiModels = {
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
export const openAiNativeModels = {
"o3-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.1,
outputPrice: 4.4,
},
// don't support tool use yet
o1: {
maxTokens: 100_000,
@@ -344,16 +393,16 @@ export const openAiNativeModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 12,
inputPrice: 1.1,
outputPrice: 4.4,
},
"gpt-4o": {
maxTokens: 4_096,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
inputPrice: 2.5,
outputPrice: 10,
},
"gpt-4o-mini": {
maxTokens: 16_384,
@@ -397,13 +446,284 @@ export const deepSeekModels = {
},
} as const satisfies Record<string, ModelInfo>
// Qwen
// https://bailian.console.aliyun.com/
export type QwenModelId = keyof typeof qwenModels
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
export const qwenModels = {
"qwen2.5-coder-32b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.002,
outputPrice: 0.006,
cacheWritesPrice: 0.002,
cacheReadsPrice: 0.006,
},
"qwen2.5-coder-14b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.002,
outputPrice: 0.006,
cacheWritesPrice: 0.002,
cacheReadsPrice: 0.006,
},
"qwen2.5-coder-7b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.001,
outputPrice: 0.002,
cacheWritesPrice: 0.001,
cacheReadsPrice: 0.002,
},
"qwen2.5-coder-3b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen2.5-coder-1.5b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen2.5-coder-0.5b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen-coder-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.5,
outputPrice: 7,
cacheWritesPrice: 3.5,
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
},
"qwen-max-latest": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.4,
outputPrice: 9.6,
cacheWritesPrice: 2.4,
cacheReadsPrice: 9.6,
},
"qwen-coder-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.5,
outputPrice: 7,
cacheWritesPrice: 3.5,
cacheReadsPrice: 7,
},
"qwen-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
},
"qwen-turbo": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
},
"qwen-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.4,
outputPrice: 9.6,
cacheWritesPrice: 2.4,
cacheReadsPrice: 9.6,
},
"deepseek-v3": {
maxTokens: 8_000,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0.28,
cacheWritesPrice: 0.14,
cacheReadsPrice: 0.014,
},
"deepseek-r1": {
maxTokens: 8_000,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 2.19,
cacheWritesPrice: 0.55,
cacheReadsPrice: 0.14,
},
"qwen-vl-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 9,
cacheWritesPrice: 3,
cacheReadsPrice: 9,
},
"qwen-vl-max-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 9,
cacheWritesPrice: 3,
cacheReadsPrice: 9,
},
"qwen-vl-plus": {
maxTokens: 6_000,
contextWindow: 8_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.5,
outputPrice: 4.5,
cacheWritesPrice: 1.5,
cacheReadsPrice: 4.5,
},
"qwen-vl-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.5,
outputPrice: 4.5,
cacheWritesPrice: 1.5,
cacheReadsPrice: 4.5,
},
} as const satisfies Record<string, ModelInfo>
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = keyof typeof mistralModels
export const mistralDefaultModelId: MistralModelId = "codestral-latest"
export const mistralDefaultModelId: MistralModelId = "codestral-2501"
export const mistralModels = {
"codestral-latest": {
maxTokens: 32_768,
"mistral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"pixtral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"ministral-3b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.04,
outputPrice: 0.04,
},
"ministral-8b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.1,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.3,
},
"pixtral-12b-2409": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-mistral-nemo-2407": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-codestral-mamba": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"codestral-2501": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
@@ -411,3 +731,16 @@ export const mistralModels = {
outputPrice: 0.9,
},
} as const satisfies Record<string, ModelInfo>
// LiteLLM
// https://docs.litellm.ai/docs/
export type LiteLLMModelId = string
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
}
+29 -23
View File
@@ -7,42 +7,48 @@ Mention regex:
- **Regex Breakdown**:
- `/@`:
- **@**: The mention must start with the '@' symbol.
- **@**: The mention must start with the '@' symbol.
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`:
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
- `(?:\/|\w+:\/\/)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
- `\/`:
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
- `|`: Logical OR.
- `\w+:\/\/`:
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
- `[^\s]+?`:
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
- `|`: Logical OR.
- `problems\b`:
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
- `(?:\/|\w+:\/\/)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
- `\/`:
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
- `|`: Logical OR.
- `\w+:\/\/`:
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
- `[^\s]+?`:
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
- `|`: Logical OR.
- `problems\b`:
- **Exact Word ('problems')**: Matches the exact word 'problems'.
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
- `terminal\b`:
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
- `[.,;:!?]?`:
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
- `(?=[\s\r\n]|$)`:
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
- `[.,;:!?]?`:
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
- `(?=[\s\r\n]|$)`:
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
- **Summary**:
- The regex effectively matches:
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
- The exact word 'problems'.
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
- The exact word 'problems'.
- The exact word 'terminal'.
- The exact word 'git-changes'.
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
- **Global Regex**:
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
*/
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegex =
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
+36
View File
@@ -66,3 +66,39 @@ export type McpToolCallResponse = {
>
isError?: boolean
}
export interface McpMarketplaceItem {
mcpId: string
githubUrl: string
name: string
author: string
description: string
codiconIcon: string
logoUrl: string
category: string
tags: string[]
requiresApiKey: boolean
readmeContent?: string
llmsInstallationContent?: string
isRecommended: boolean
githubStars: number
downloadCount: number
createdAt: string
updatedAt: string
lastGithubSync: string
}
export interface McpMarketplaceCatalog {
items: McpMarketplaceItem[]
}
export interface McpDownloadResponse {
mcpId: string
githubUrl: string
name: string
author: string
description: string
readmeContent: string
llmsInstallationContent: string
requiresApiKey: boolean
}
+240
View File
@@ -0,0 +1,240 @@
import { describe, it, beforeEach, afterEach } from "mocha"
import { expect } from "chai"
import { getShell } from "../utils/shell"
import * as vscode from "vscode"
import { userInfo } from "os"
describe("Shell Detection Tests", () => {
let originalPlatform: string
let originalEnv: NodeJS.ProcessEnv
let originalGetConfig: any
let originalUserInfo: any
// Helper to mock VS Code configuration
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
vscode.workspace.getConfiguration = () =>
({
get: (key: string) => {
if (key === `defaultProfile.${platformKey}`) {
return defaultProfileName
}
if (key === `profiles.${platformKey}`) {
return profiles
}
return undefined
},
}) as any
}
beforeEach(() => {
// Store original references
originalPlatform = process.platform
originalEnv = { ...process.env }
originalGetConfig = vscode.workspace.getConfiguration
originalUserInfo = userInfo
// Clear environment variables for a clean test
delete process.env.SHELL
delete process.env.COMSPEC
// Default userInfo() mock
;(userInfo as any) = () => ({ shell: null })
})
afterEach(() => {
// Restore everything
Object.defineProperty(process, "platform", { value: originalPlatform })
process.env = originalEnv
vscode.workspace.getConfiguration = originalGetConfig
;(userInfo as any) = originalUserInfo
})
// --------------------------------------------------------------------------
// Windows Shell Detection
// --------------------------------------------------------------------------
describe("Windows Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "win32" })
})
it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
})
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { source: "PowerShell" },
})
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("falls back to legacy PowerShell if profile includes 'powershell' but no path/source", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: {},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
})
it("handles undefined shell profile gracefully", () => {
mockVsCodeConfig("windows", "NonExistentProfile", {})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("uses WSL bash when profile indicates WSL source", () => {
mockVsCodeConfig("windows", "WSL", {
WSL: { source: "WSL" },
})
expect(getShell()).to.equal("/bin/bash")
})
it("uses WSL bash when profile name includes 'wsl'", () => {
mockVsCodeConfig("windows", "Ubuntu WSL", {
"Ubuntu WSL": {},
})
expect(getShell()).to.equal("/bin/bash")
})
it("defaults to cmd.exe if no special profile is matched", () => {
mockVsCodeConfig("windows", "CommandPrompt", {
CommandPrompt: {},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("respects userInfo() if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" })
expect(getShell()).to.equal("C:\\Custom\\PowerShell.exe")
})
it("respects an odd COMSPEC if no userInfo shell is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe"
expect(getShell()).to.equal("D:\\CustomCmd\\cmd.exe")
})
})
// --------------------------------------------------------------------------
// macOS Shell Detection
// --------------------------------------------------------------------------
describe("macOS Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "darwin" })
})
it("uses VS Code profile path if available", () => {
mockVsCodeConfig("osx", "MyCustomShell", {
MyCustomShell: { path: "/usr/local/bin/fish" },
})
expect(getShell()).to.equal("/usr/local/bin/fish")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/opt/homebrew/bin/zsh" })
expect(getShell()).to.equal("/opt/homebrew/bin/zsh")
})
it("falls back to SHELL env var if no userInfo shell is found", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.SHELL = "/usr/local/bin/zsh"
expect(getShell()).to.equal("/usr/local/bin/zsh")
})
it("falls back to /bin/zsh if no config, userInfo, or env variable is set", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
// userInfo => null, SHELL => undefined
expect(getShell()).to.equal("/bin/zsh")
})
})
// --------------------------------------------------------------------------
// Linux Shell Detection
// --------------------------------------------------------------------------
describe("Linux Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "linux" })
})
it("uses VS Code profile path if available", () => {
mockVsCodeConfig("linux", "CustomProfile", {
CustomProfile: { path: "/usr/bin/fish" },
})
expect(getShell()).to.equal("/usr/bin/fish")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/usr/bin/zsh" })
expect(getShell()).to.equal("/usr/bin/zsh")
})
it("falls back to SHELL env var if no userInfo shell is found", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.SHELL = "/usr/bin/fish"
expect(getShell()).to.equal("/usr/bin/fish")
})
it("falls back to /bin/bash if nothing is set", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
// userInfo => null, SHELL => undefined
expect(getShell()).to.equal("/bin/bash")
})
})
// --------------------------------------------------------------------------
// Unknown Platform & Error Handling
// --------------------------------------------------------------------------
describe("Unknown Platform / Error Handling", () => {
it("falls back to /bin/sh for unknown platforms", () => {
Object.defineProperty(process, "platform", { value: "sunos" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
expect(getShell()).to.equal("/bin/sh")
})
it("handles VS Code config errors gracefully, falling back to userInfo shell if present", () => {
Object.defineProperty(process, "platform", { value: "linux" })
vscode.workspace.getConfiguration = () => {
throw new Error("Configuration error")
}
;(userInfo as any) = () => ({ shell: "/bin/bash" })
expect(getShell()).to.equal("/bin/bash")
})
it("handles userInfo errors gracefully, falling back to environment variable if present", () => {
Object.defineProperty(process, "platform", { value: "darwin" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => {
throw new Error("userInfo error")
}
process.env.SHELL = "/bin/zsh"
expect(getShell()).to.equal("/bin/zsh")
})
it("falls back fully to default shell paths if everything fails", () => {
Object.defineProperty(process, "platform", { value: "linux" })
vscode.workspace.getConfiguration = () => {
throw new Error("Configuration error")
}
;(userInfo as any) = () => {
throw new Error("userInfo error")
}
// No SHELL in env
delete process.env.SHELL
expect(getShell()).to.equal("/bin/bash")
})
})
})
+10
View File
@@ -34,4 +34,14 @@ describe("Extension Tests", function () {
await vscode.commands.executeCommand("cline.historyButtonClicked")
// Success if no error thrown
})
it("should handle advanced settings configuration", async () => {
// Test browser session setting
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
const updatedConfig = vscode.workspace.getConfiguration("cline")
expect(updatedConfig.get("disableBrowserTool")).to.be.true
// Reset settings
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
})
})
+27 -2
View File
@@ -28,7 +28,13 @@ describe("Chat Integration Tests", () => {
vscode.postMessage({ type: 'newTask', text: message.text });
break;
case 'toggleMode':
vscode.postMessage({ type: 'chatSettings', chatSettings: { mode: 'act' } });
vscode.postMessage({
type: 'togglePlanActMode',
chatSettings: { mode: 'act' },
chatContent: {
message: "message test",
}
});
break;
case 'invoke':
if (message.invoke === 'primaryButtonClick') {
@@ -78,7 +84,7 @@ describe("Chat Integration Tests", () => {
// Set up state change listener
const stateChangePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "chatSettings") {
if (message.type === "togglePlanActMode") {
resolve(message)
}
})
@@ -92,6 +98,25 @@ describe("Chat Integration Tests", () => {
assert.equal(stateChange.chatSettings.mode, "act")
})
it("should toggle between plan and act modes with messages", async () => {
// Set up state change listener
const stateChangePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "togglePlanActMode") {
resolve(message)
}
})
})
// Trigger mode toggle
await panel.webview.postMessage({ type: "toggleMode" })
// Verify mode changed
const stateChange = await stateChangePromise
assert.equal(stateChange.chatSettings.mode, "act")
assert.equal(stateChange.chatContent.message, "message test")
})
it("should handle tool approval flow", async () => {
// Set up approval listener
const approvalPromise = new Promise<any>((resolve) => {
+177
View File
@@ -0,0 +1,177 @@
import { exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)
const GIT_OUTPUT_LINE_LIMIT = 500
export interface GitCommit {
hash: string
shortHash: string
subject: string
author: string
date: string
}
async function checkGitRepo(cwd: string): Promise<boolean> {
try {
await execAsync("git rev-parse --git-dir", { cwd })
return true
} catch (error) {
return false
}
}
async function checkGitInstalled(): Promise<boolean> {
try {
await execAsync("git --version")
return true
} catch (error) {
return false
}
}
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
try {
const isInstalled = await checkGitInstalled()
if (!isInstalled) {
console.error("Git is not installed")
return []
}
const isRepo = await checkGitRepo(cwd)
if (!isRepo) {
console.error("Not a git repository")
return []
}
// Search commits by hash or message, limiting to 10 results
const { stdout } = await execAsync(
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
{ cwd },
)
let output = stdout
if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
// If no results from grep search and query looks like a hash, try searching by hash
const { stdout: hashStdout } = await execAsync(
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
{ cwd },
).catch(() => ({ stdout: "" }))
if (!hashStdout.trim()) {
return []
}
output = hashStdout
}
const commits: GitCommit[] = []
const lines = output
.trim()
.split("\n")
.filter((line) => line !== "--")
for (let i = 0; i < lines.length; i += 5) {
commits.push({
hash: lines[i],
shortHash: lines[i + 1],
subject: lines[i + 2],
author: lines[i + 3],
date: lines[i + 4],
})
}
return commits
} catch (error) {
console.error("Error searching commits:", error)
return []
}
}
export async function getCommitInfo(hash: string, cwd: string): Promise<string> {
try {
const isInstalled = await checkGitInstalled()
if (!isInstalled) {
return "Git is not installed"
}
const isRepo = await checkGitRepo(cwd)
if (!isRepo) {
return "Not a git repository"
}
// Get commit info, stats, and diff separately
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
cwd,
})
const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
const summary = [
`Commit: ${shortHash} (${fullHash})`,
`Author: ${author}`,
`Date: ${date}`,
`\nMessage: ${subject}`,
body ? `\nDescription:\n${body}` : "",
"\nFiles Changed:",
stats.trim(),
"\nFull Changes:",
].join("\n")
const output = summary + "\n\n" + diff.trim()
return truncateOutput(output)
} catch (error) {
console.error("Error getting commit info:", error)
return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
}
}
export async function getWorkingState(cwd: string): Promise<string> {
try {
const isInstalled = await checkGitInstalled()
if (!isInstalled) {
return "Git is not installed"
}
const isRepo = await checkGitRepo(cwd)
if (!isRepo) {
return "Not a git repository"
}
// Get status of working directory
const { stdout: status } = await execAsync("git status --short", { cwd })
if (!status.trim()) {
return "No changes in working directory"
}
// Get all changes (both staged and unstaged) compared to HEAD
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
return truncateOutput(output)
} catch (error) {
console.error("Error getting working state:", error)
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
}
}
function truncateOutput(content: string): string {
if (!GIT_OUTPUT_LINE_LIMIT) {
return content
}
const lines = content.split("\n")
if (lines.length <= GIT_OUTPUT_LINE_LIMIT) {
return content
}
const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before
const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after
return [
...lines.slice(0, beforeLimit),
`\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`,
...lines.slice(-afterLimit),
].join("\n")
}
+227
View File
@@ -0,0 +1,227 @@
import * as vscode from "vscode"
import { userInfo } from "os"
const SHELL_PATHS = {
// Windows paths
POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
CMD: "C:\\Windows\\System32\\cmd.exe",
WSL_BASH: "/bin/bash",
// Unix paths
MAC_DEFAULT: "/bin/zsh",
LINUX_DEFAULT: "/bin/bash",
CSH: "/bin/csh",
BASH: "/bin/bash",
KSH: "/bin/ksh",
SH: "/bin/sh",
ZSH: "/bin/zsh",
DASH: "/bin/dash",
TCSH: "/bin/tcsh",
FALLBACK: "/bin/sh",
} as const
interface MacTerminalProfile {
path?: string
}
type MacTerminalProfiles = Record<string, MacTerminalProfile>
interface WindowsTerminalProfile {
path?: string
source?: "PowerShell" | "WSL"
}
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
interface LinuxTerminalProfile {
path?: string
}
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
// -----------------------------------------------------
// 1) VS Code Terminal Configuration Helpers
// -----------------------------------------------------
function getWindowsTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.windows")
const profiles = config.get<WindowsTerminalProfiles>("profiles.windows") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles }
}
}
function getMacTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.osx")
const profiles = config.get<MacTerminalProfiles>("profiles.osx") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as MacTerminalProfiles }
}
}
function getLinuxTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.linux")
const profiles = config.get<LinuxTerminalProfiles>("profiles.linux") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles }
}
}
// -----------------------------------------------------
// 2) Platform-Specific VS Code Shell Retrieval
// -----------------------------------------------------
/** Attempts to retrieve a shell path from VS Code config on Windows. */
function getWindowsShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
// If the profile name indicates PowerShell, do version-based detection.
// In testing it was found these typically do not have a path, and this
// implementation manages to deductively get the corect version of PowerShell
if (defaultProfileName.toLowerCase().includes("powershell")) {
if (profile?.path) {
// If there's an explicit PowerShell path, return that
return profile.path
} else if (profile?.source === "PowerShell") {
// If the profile is sourced from PowerShell, assume the newest
return SHELL_PATHS.POWERSHELL_7
}
// Otherwise, assume legacy Windows PowerShell
return SHELL_PATHS.POWERSHELL_LEGACY
}
// If there's a specific path, return that immediately
if (profile?.path) {
return profile.path
}
// If the profile indicates WSL
if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) {
return SHELL_PATHS.WSL_BASH
}
// If nothing special detected, we assume cmd
return SHELL_PATHS.CMD
}
/** Attempts to retrieve a shell path from VS Code config on macOS. */
function getMacShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getMacTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
}
/** Attempts to retrieve a shell path from VS Code config on Linux. */
function getLinuxShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getLinuxTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
}
// -----------------------------------------------------
// 3) General Fallback Helpers
// -----------------------------------------------------
/**
* Tries to get a users shell from os.userInfo() (works on Unix if the
* underlying system call is supported). Returns null on error or if not found.
*/
function getShellFromUserInfo(): string | null {
try {
const { shell } = userInfo()
return shell || null
} catch {
return null
}
}
/** Returns the environment-based shell variable, or null if not set. */
function getShellFromEnv(): string | null {
const { env } = process
if (process.platform === "win32") {
// On Windows, COMSPEC typically holds cmd.exe
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
}
if (process.platform === "darwin") {
// On macOS/Linux, SHELL is commonly the environment variable
return env.SHELL || "/bin/zsh"
}
if (process.platform === "linux") {
// On Linux, SHELL is commonly the environment variable
return env.SHELL || "/bin/bash"
}
return null
}
// -----------------------------------------------------
// 4) Publicly Exposed Shell Getter
// -----------------------------------------------------
export function getShell(): string {
// 1. Check VS Code config first.
if (process.platform === "win32") {
// Special logic for Windows
const windowsShell = getWindowsShellFromVSCode()
if (windowsShell) {
return windowsShell
}
} else if (process.platform === "darwin") {
// macOS from VS Code
const macShell = getMacShellFromVSCode()
if (macShell) {
return macShell
}
} else if (process.platform === "linux") {
// Linux from VS Code
const linuxShell = getLinuxShellFromVSCode()
if (linuxShell) {
return linuxShell
}
}
// 2. If no shell from VS Code, try userInfo()
const userInfoShell = getShellFromUserInfo()
if (userInfoShell) {
return userInfoShell
}
// 3. If still nothing, try environment variable
const envShell = getShellFromEnv()
if (envShell) {
return envShell
}
// 4. Finally, fall back to a default
if (process.platform === "win32") {
// On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system.
// Use CMD as a last resort
return SHELL_PATHS.CMD
}
// On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method.
return SHELL_PATHS.FALLBACK
}
+16
View File
@@ -0,0 +1,16 @@
// "Official" jest workaround for mocking window.matchMedia()
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // Deprecated
removeListener: vi.fn(), // Deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
+3069 -304
View File
File diff suppressed because it is too large Load Diff
+15 -9
View File
@@ -3,17 +3,13 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.101",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@floating-ui/react": "^0.27.4",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -34,7 +30,8 @@
"scripts": {
"start": "react-scripts start",
"build": "node ./scripts/build-react-no-split.js",
"test": "react-scripts test",
"test": "vitest run",
"test:watch": "vitest dev",
"eject": "react-scripts eject"
},
"eslintConfig": {
@@ -56,6 +53,15 @@
]
},
"devDependencies": {
"@types/vscode-webview": "^1.57.5"
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^15.0.6",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^20.x",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/vscode-webview": "^1.57.5",
"jsdom": "^25.0.1",
"vitest": "^2.1.9"
}
}
+17 -9
View File
@@ -98,18 +98,26 @@ config.module.rules[1].oneOf.forEach((rule) => {
}
})
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
default: false,
// Force all code into a single bundle for VS Code webview compatibility.
// This is necessary for:
// 1. Mermaid.js to work properly (prevents async chunk loading)
// 2. Consistent CSP nonce handling (single bundle = single nonce)
config.optimization = {
...config.optimization,
splitChunks: {
cacheGroups: {
default: false,
},
name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting
},
runtimeChunk: false,
}
// Disable code chunks
config.optimization.runtimeChunk = false
// Rename main.{hash}.js to main.js
config.output.filename = "static/js/[name].js"
// Ensure all chunks are named 'main' to match our CSP nonce setup
config.output = {
...config.output,
filename: "static/js/[name].js",
}
// Rename main.{hash}.css to main.css
config.plugins[5].options.filename = "static/css/[name].css"
+2
View File
@@ -0,0 +1,2 @@
import "@testing-library/jest-dom"
import "./matchMedia"
+25 -16
View File
@@ -1,6 +1,7 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "../../utils/vscStyles"
import { vscode } from "../../utils/vscode"
interface AnnouncementProps {
version: string
@@ -30,29 +31,34 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
<b>Plan/Act mode toggle:</b> Plan mode turns Cline into an architect that gathers information, asks clarifying
questions, and designs a solution. Switch back to Act mode to let him execute the plan!{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1881761978986934582" style={{ display: "inline" }}>
See a demo here.
<b>Introducing MCP Marketplace:</b> Discover and install the best MCP servers right from the extension, with
new servers added regularly! Get started by going to the{" "}
<span className="codicon codicon-extensions" style={{ marginRight: "4px", fontSize: 10 }}></span>
<VSCodeLink
onClick={() => {
vscode.postMessage({ type: "showMcpView" })
}}>
MCP Servers tab
</VSCodeLink>
.
</li>
<li>
<b>Quick API/model switching</b> with a new popup menu under the chat field
<b>Mermaid diagrams in Plan mode!</b> Cline can now visualize his plans using flowcharts, sequences,
entity-relationships, and more. When he explains his approach using mermaid, you'll see a diagram right in
chat that you can click to expand.
</li>
<li>
<b>VS Code LM API</b> lets you use models from other extensions like GitHub Copilot
Use <code>@terminal</code> to reference terminal contents, and <code>@git</code> to reference working changes
and commits!
</li>
<li>
<b>MCP server improvements:</b> On/off toggle to disable servers when not in use, and Auto-approve option for
individual tools
</li>
<li>
In case you missed it, Cline now supports Checkpoints!{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1876378124126236949" style={{ display: "inline" }}>
See it in action here.
</VSCodeLink>
New visual indicator for checkpoints after edits & commands, and automatic checkpoint at the start of each
task.
</li>
</ul>
<VSCodeLink href="https://x.com/sdrzn/status/1892262424881090721" style={{ display: "inline" }}>
See a demo of the changes here!
</VSCodeLink>
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
@@ -109,9 +115,12 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}}
/>
<p style={{ margin: "0" }}>
Join our{" "}
Join us on{" "}
<VSCodeLink style={{ display: "inline" }} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
discord
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={{ display: "inline" }} href="https://www.reddit.com/r/cline/">
@@ -169,7 +169,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
// }}
onClick={(e) => {
/*
vscode web toolkit bug: when changing the value of a vscodecheckbox programatically, it will call its onChange with stale state. This led to updateEnabled being called with an old vesion of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and intead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
*/
if (!hasEnabledActions) return
e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing
@@ -1,17 +1,16 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { BrowserSettingsMenu } from "../browser/BrowserSettingsMenu"
import { CheckpointControls } from "../common/CheckpointControls"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ChatRowContent, ProgressIndicator } from "./ChatRow"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import { findLast } from "../../../../src/shared/array"
import { BrowserSettingsMenu } from "../browser/BrowserSettingsMenu"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
interface BrowserSessionRowProps {
messages: ClineMessage[]
@@ -144,10 +143,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
return launchMessage?.say === "browser_action_launch"
}, [messages])
const lastCheckpointMessageTs = useMemo(() => {
const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
return lastCheckpointMessage?.ts
}, [messages])
// const lastCheckpointMessageTs = useMemo(() => {
// const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
// return lastCheckpointMessage?.ts
// }, [messages])
// Find the latest available URL and screenshot
const latestState = useMemo(() => {
@@ -231,10 +230,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Use latest click position while browsing, otherwise use display state
const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition
let shouldShowCheckpoints = true
if (isLast) {
shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
}
// let shouldShowCheckpoints = true
// if (isLast) {
// shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
// }
const shouldShowSettings = useMemo(() => {
const lastMessage = messages[messages.length - 1]
@@ -423,7 +422,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
</div>
)}
{shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />}
{/* {shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />} */}
</BrowserSessionRowContainer>,
)
+122 -46
View File
@@ -13,7 +13,7 @@ import {
} from "../../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/shared/combineCommandSequences"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
@@ -24,6 +24,7 @@ import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import { highlightMentions } from "./TaskHeader"
import { CheckmarkControl } from "../common/CheckmarkControl"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -59,8 +60,8 @@ const ChatRow = memo(
message.ask === "tool" ||
message.say === "command" ||
message.ask === "command" ||
message.say === "completion_result" ||
message.ask === "completion_result" ||
// message.say === "completion_result" ||
// message.ask === "completion_result" ||
message.say === "use_mcp_server" ||
message.ask === "use_mcp_server")
@@ -99,7 +100,7 @@ const ChatRow = memo(
export default ChatRow
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
const { mcpServers } = useExtensionState()
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
@@ -185,9 +186,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
marginBottom: "-1.5px",
}}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
{message.type === "ask" ? "Cline wants to execute this command:" : "Cline executed this command:"}
</span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline wants to execute this command:</span>,
]
case "use_mcp_server":
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
@@ -202,18 +201,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
marginBottom: "-1.5px",
}}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
{message.type === "ask" ? (
<>
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code>{mcpServerUse.serverName}</code> MCP server:
</>
) : (
<>
Cline {mcpServerUse.type === "use_mcp_tool" ? "used a tool" : "accessed a resource"} on the{" "}
<code>{mcpServerUse.serverName}</code> MCP server:
</>
)}
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code style={{ wordBreak: "break-all" }}>
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
</code>{" "}
MCP server:
</span>,
]
case "completion_result":
@@ -298,16 +291,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
default:
return [null, null]
}
}, [
type,
cost,
apiRequestFailedMessage,
isCommandExecuting,
apiReqCancelReason,
isMcpServerResponding,
message.text,
message.type,
])
}, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason, isMcpServerResponding, message.text])
const headerStyle: React.CSSProperties = {
display: "flex",
@@ -346,9 +330,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<>
<div style={headerStyle}>
{toolIcon("edit")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? "Cline wants to edit this file:" : "Cline is editing this file:"}
</span>
<span style={{ fontWeight: "bold" }}>Cline wants to edit this file:</span>
</div>
<CodeAccordian
// isLoading={message.partial}
@@ -364,9 +346,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<>
<div style={headerStyle}>
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? "Cline wants to create a new file:" : "Cline is creating a new file:"}
</span>
<span style={{ fontWeight: "bold" }}>Cline wants to create a new file:</span>
</div>
<CodeAccordian
isLoading={message.partial}
@@ -383,7 +363,8 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<div style={headerStyle}>
{toolIcon("file-code")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? "Cline wants to read this file:" : "Cline read this file:"}
{/* {message.type === "ask" ? "" : "Cline read this file:"} */}
Cline wants to read this file:
</span>
</div>
{/* <CodeAccordian
@@ -505,15 +486,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<div style={headerStyle}>
{toolIcon("search")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? (
<>
Cline wants to search this directory for <code>{tool.regex}</code>:
</>
) : (
<>
Cline searched this directory for <code>{tool.regex}</code>:
</>
)}
Cline wants to search this directory for <code>{tool.regex}</code>:
</span>
</div>
<CodeAccordian
@@ -751,7 +724,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
}}>
{icon}
{title}
{/* Need to render this everytime since it affects height of row by 2px */}
{/* Need to render this every time since it affects height of row by 2px */}
<VSCodeBadge
style={{
opacity: cost != null && cost > 0 ? 1 : 0,
@@ -842,6 +815,62 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<Markdown markdown={message.text} />
</div>
)
case "reasoning":
return (
<>
{message.text && (
<div
onClick={onToggleExpand}
style={{
// marginBottom: 15,
cursor: "pointer",
color: "var(--vscode-descriptionForeground)",
fontStyle: "italic",
overflow: "hidden",
}}>
{isExpanded ? (
<div style={{ marginTop: -3 }}>
<span style={{ fontWeight: "bold", display: "block", marginBottom: "4px" }}>
Reasoning
<span
className="codicon codicon-chevron-down"
style={{
display: "inline-block",
transform: "translateY(3px)",
marginLeft: "1.5px",
}}
/>
</span>
{message.text}
</div>
) : (
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ fontWeight: "bold", marginRight: "4px" }}>Reasoning:</span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
direction: "rtl",
textAlign: "left",
flex: 1,
}}>
{message.text + "\u200E"}
</span>
<span
className="codicon codicon-chevron-right"
style={{
marginLeft: "4px",
flexShrink: 0,
}}
/>
</div>
)}
</div>
)}
</>
)
case "user_feedback":
return (
<div
@@ -933,6 +962,53 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
</>
)
case "clineignore_error":
return (
<>
<div
style={{
display: "flex",
flexDirection: "column",
backgroundColor: "rgba(255, 191, 0, 0.1)",
padding: 8,
borderRadius: 3,
fontSize: 12,
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: 4,
}}>
<i
className="codicon codicon-error"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
}}></i>
<span
style={{
fontWeight: 500,
color: "#FFA500",
}}>
Access Denied
</span>
</div>
<div>
Cline tried to access <code>{message.text}</code> which is blocked by the{" "}
<code>.clineignore</code>
file.
</div>
</div>
</>
)
case "checkpoint_created":
return (
<>
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} />
</>
)
case "completion_result":
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
+94 -16
View File
@@ -1,9 +1,10 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
import { useClickAway, useWindowSize } from "react-use"
import { useClickAway, useEvent, useWindowSize } from "react-use"
import styled from "styled-components"
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
ContextMenuOptionType,
@@ -12,13 +13,16 @@ import {
removeMention,
shouldShowContextMenu,
} from "../../utils/context-mentions"
import { useMetaKeyDetection, useShortcut } from "../../utils/hooks"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import Thumbnails from "../common/Thumbnails"
import Tooltip from "../common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { ChatSettings } from "../../../../src/shared/ChatSettings"
interface ChatTextAreaProps {
inputValue: string
@@ -210,8 +214,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels } = useExtensionState()
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [gitCommits, setGitCommits] = useState<any[]>([])
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
@@ -231,13 +237,47 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const buttonRef = useRef<HTMLDivElement>(null)
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
const [, metaKeyChar] = useMetaKeyDetection(platform)
// Add a ref to track previous menu state
const prevShowModelSelector = useRef(showModelSelector)
// Fetch git commits when Git is selected or when typing a hash
useEffect(() => {
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
vscode.postMessage({
type: "searchCommits",
text: searchQuery || "",
})
}
}, [selectedType, searchQuery])
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "commitSearchResults": {
const commits =
message.commits?.map((commit: any) => ({
type: ContextMenuOptionType.Git,
value: commit.hash,
label: commit.subject,
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
})) || []
setGitCommits(commits)
break
}
}
}, [])
useEvent("message", handleMessage)
const queryItems = useMemo(() => {
return [
{ type: ContextMenuOptionType.Problems, value: "problems" },
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
...gitCommits,
...filePaths
.map((file) => "/" + file)
.map((path) => ({
@@ -245,7 +285,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
value: path,
})),
]
}, [filePaths])
}, [filePaths, gitCommits])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -269,7 +309,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return
}
if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
if (
type === ContextMenuOptionType.File ||
type === ContextMenuOptionType.Folder ||
type === ContextMenuOptionType.Git
) {
if (!value) {
setSelectedType(type)
setSearchQuery("")
@@ -288,6 +332,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
insertValue = value || ""
} else if (type === ContextMenuOptionType.Problems) {
insertValue = "problems"
} else if (type === ContextMenuOptionType.Terminal) {
insertValue = "terminal"
} else if (type === ContextMenuOptionType.Git) {
insertValue = value || ""
}
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
@@ -377,7 +425,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
const charAfterIsWhitespace =
charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
// checks if char before cusor is whitespace after a mention
// checks if char before cursor is whitespace after a mention
if (
charBeforeIsWhitespace &&
inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string
@@ -597,7 +645,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}, [apiConfiguration, openRouterModels])
const onModeToggle = useCallback(() => {
if (textAreaDisabled) return
// if (textAreaDisabled) return
let changeModeDelay = 0
if (showModelSelector) {
// user has model selector open, so we should save it before switching modes
@@ -607,17 +655,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setTimeout(() => {
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
vscode.postMessage({
type: "chatSettings",
type: "togglePlanActMode",
chatSettings: {
mode: newMode,
},
chatContent: {
message: inputValue.trim() ? inputValue : undefined,
images: selectedImages.length > 0 ? selectedImages : undefined,
},
})
// Focus the textarea after mode toggle with slight delay
setTimeout(() => {
textAreaRef.current?.focus()
}, 100)
}, changeModeDelay)
}, [chatSettings.mode, textAreaDisabled, showModelSelector, submitApiConfig])
}, [chatSettings.mode, showModelSelector, submitApiConfig, inputValue, selectedImages])
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
const handleContextButtonClick = useCallback(() => {
if (textAreaDisabled) return
@@ -695,6 +749,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return `openai-compat:${selectedModelId}`
case "vscode-lm":
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
case "together":
return `${selectedProvider}:${apiConfiguration.togetherModelId}`
case "lmstudio":
return `${selectedProvider}:${apiConfiguration.lmStudioModelId}`
case "ollama":
return `${selectedProvider}:${apiConfiguration.ollamaModelId}`
case "litellm":
return `${selectedProvider}:${apiConfiguration.liteLlmModelId}`
case "requesty":
return `${selectedProvider}:${apiConfiguration.requestyModelId}`
default:
return `${selectedProvider}:${selectedModelId}`
}
@@ -874,7 +938,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
borderTop: 0,
borderColor: "transparent",
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
padding: "9px 49px 3px 9px",
padding: "9px 28px 3px 9px",
}}
/>
<DynamicTextArea
@@ -1038,7 +1102,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={textAreaDisabled}
disabled={false}
onClick={handleModelButtonClick}
// onKeyDown={(e) => {
// if (e.key === "Enter" || e.key === " ") {
@@ -1067,12 +1131,26 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)}
</ModelContainer>
</ButtonGroup>
<SwitchContainer data-testid="mode-switch" disabled={textAreaDisabled} onClick={onModeToggle}>
<Slider isAct={chatSettings.mode === "act"} isPlan={chatSettings.mode === "plan"} />
<SwitchOption isActive={chatSettings.mode === "plan"}>Plan</SwitchOption>
<SwitchOption isActive={chatSettings.mode === "act"}>Act</SwitchOption>
</SwitchContainer>
<Tooltip
visible={shownTooltipMode !== null}
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
hintText={`Toggle w/ ${metaKeyChar}+Shift+A`}>
<SwitchContainer data-testid="mode-switch" disabled={false} onClick={onModeToggle}>
<Slider isAct={chatSettings.mode === "act"} isPlan={chatSettings.mode === "plan"} />
<SwitchOption
isActive={chatSettings.mode === "plan"}
onMouseOver={() => setShownTooltipMode("plan")}
onMouseLeave={() => setShownTooltipMode(null)}>
Plan
</SwitchOption>
<SwitchOption
isActive={chatSettings.mode === "act"}
onMouseOver={() => setShownTooltipMode("act")}
onMouseLeave={() => setShownTooltipMode(null)}>
Act
</SwitchOption>
</SwitchContainer>
</Tooltip>
</ControlsContainer>
</div>
)
+97 -65
View File
@@ -324,67 +324,99 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
/*
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
*/
const handlePrimaryButtonClick = useCallback(() => {
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
break
case "completion_result":
case "resume_completed_task":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask])
const handlePrimaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: trimmedInput,
images: images,
})
} else {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
case "completion_result":
case "resume_completed_task":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask],
)
const handleSecondaryButtonClick = useCallback(() => {
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
const handleSecondaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask, isStreaming])
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
text: trimmedInput,
images: images,
})
} else {
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask, isStreaming],
)
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
@@ -426,14 +458,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
handleSendMessage(message.text ?? "", message.images ?? [])
break
case "primaryButtonClick":
handlePrimaryButtonClick()
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
break
case "secondaryButtonClick":
handleSecondaryButtonClick()
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
break
}
}
// textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
// textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference.
},
[isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
)
@@ -869,7 +901,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={handlePrimaryButtonClick}>
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
@@ -881,7 +913,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={handleSecondaryButtonClick}>
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}
@@ -919,7 +951,7 @@ const ScrollToBottomButton = styled.div`
justify-content: center;
align-items: center;
flex: 1;
height: 24px;
height: 25px;
&:hover {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 90%, transparent);
+34 -2
View File
@@ -48,10 +48,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
switch (option.type) {
case ContextMenuOptionType.Problems:
return <span>Problems</span>
case ContextMenuOptionType.Terminal:
return <span>Terminal</span>
case ContextMenuOptionType.URL:
return <span>Paste URL to fetch contents</span>
case ContextMenuOptionType.NoResults:
return <span>No results found</span>
case ContextMenuOptionType.Git:
if (option.value) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
<span
style={{
fontSize: "0.85em",
opacity: 0.7,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: "1.2",
}}>
{option.description}
</span>
</div>
)
} else {
return <span>Git Commits</span>
}
case ContextMenuOptionType.File:
case ContextMenuOptionType.Folder:
if (option.value) {
@@ -85,8 +108,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
return "folder"
case ContextMenuOptionType.Problems:
return "warning"
case ContextMenuOptionType.Terminal:
return "terminal"
case ContextMenuOptionType.URL:
return "link"
case ContextMenuOptionType.Git:
return "git-commit"
case ContextMenuOptionType.NoResults:
return "info"
default:
@@ -161,7 +188,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
/>
{renderOptionContent(option)}
</div>
{(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
{(option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.Git) &&
!option.value && (
<i
className="codicon codicon-chevron-right"
@@ -173,7 +202,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
/>
)}
{(option.type === ContextMenuOptionType.Problems ||
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
option.type === ContextMenuOptionType.Terminal ||
((option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.Git) &&
option.value)) && (
<i
className="codicon codicon-add"
+11 -5
View File
@@ -100,20 +100,26 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}, [task.text, windowWidth])
const isCostAvailable = useMemo(() => {
const openAiCompatHasPricing =
apiConfiguration?.apiProvider === "openai" &&
apiConfiguration?.openAiModelInfo?.inputPrice &&
apiConfiguration?.openAiModelInfo?.outputPrice
if (openAiCompatHasPricing) {
return true
}
return (
apiConfiguration?.apiProvider !== "openai" &&
apiConfiguration?.apiProvider !== "vscode-lm" &&
apiConfiguration?.apiProvider !== "ollama" &&
apiConfiguration?.apiProvider !== "lmstudio" &&
apiConfiguration?.apiProvider !== "gemini"
)
}, [apiConfiguration?.apiProvider])
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
const ContextWindowComponent = (
<>
{isTaskExpanded && contextWindow && lastApiReqTotalTokens && (
{isTaskExpanded && contextWindow && (
<div
style={{
display: "flex",
@@ -140,7 +146,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
flex: 1,
whiteSpace: "nowrap",
}}>
<span>{formatLargeNumber(lastApiReqTotalTokens)}</span>
<span>{formatLargeNumber(lastApiReqTotalTokens || 0)}</span>
<div
style={{
display: "flex",
@@ -158,7 +164,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}}>
<div
style={{
width: `${(lastApiReqTotalTokens / contextWindow) * 100}%`,
width: `${((lastApiReqTotalTokens || 0) / contextWindow) * 100}%`,
height: "100%",
backgroundColor: "var(--vscode-badge-foreground)",
borderRadius: "2px",
@@ -0,0 +1,39 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import Announcement from "../Announcement"
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
useTheme: () => ({ themeType: "light" }),
VSCodeButton: (props: any) => <button {...props}>{props.children}</button>,
VSCodeLink: ({ children }: { children: React.ReactNode }) => <a>{children}</a>,
}))
describe("Announcement", () => {
const hideAnnouncement = vi.fn()
it("renders the announcement with the correct version", () => {
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
expect(screen.getByText(/New in v2.0/)).toBeInTheDocument()
})
it("calls hideAnnouncement when close button is clicked", () => {
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
fireEvent.click(screen.getByRole("button"))
expect(hideAnnouncement).toHaveBeenCalled()
})
it("renders the mcp server improvements announcement", () => {
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
expect(screen.getByText(/MCP server improvements:/)).toBeInTheDocument()
})
it("renders the 'See new changes' button feature", () => {
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
expect(screen.getByText(/See it in action here./)).toBeInTheDocument()
})
it("renders the demo link", () => {
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
expect(screen.getByText(/See a demo here./)).toBeInTheDocument()
})
})
@@ -0,0 +1,394 @@
import { useCallback, useRef, useState, useEffect } from "react"
import { useClickAway, useEvent } from "react-use"
import styled from "styled-components"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { vscode } from "../../utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
import { ClineCheckpointRestore } from "../../../../src/shared/WebviewMessage"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { createPortal } from "react-dom"
import { useFloating, offset, flip, shift } from "@floating-ui/react"
interface CheckmarkControlProps {
messageTs?: number
isCheckpointCheckedOut?: boolean
}
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: CheckmarkControlProps) => {
const [compareDisabled, setCompareDisabled] = useState(false)
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { refs, floatingStyles, update, placement } = useFloating({
placement: "bottom-end",
middleware: [
offset({
mainAxis: 8,
crossAxis: 10,
}),
flip(),
shift(),
],
})
useEffect(() => {
const handleScroll = () => {
update()
}
window.addEventListener("scroll", handleScroll, true)
return () => window.removeEventListener("scroll", handleScroll, true)
}, [update])
useEffect(() => {
if (showRestoreConfirm) {
update()
}
}, [showRestoreConfirm, update])
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
}
}, [])
const handleRestoreTask = () => {
setRestoreTaskDisabled(true)
vscode.postMessage({
type: "checkpointRestore",
number: messageTs,
text: "task",
})
}
const handleRestoreWorkspace = () => {
setRestoreWorkspaceDisabled(true)
vscode.postMessage({
type: "checkpointRestore",
number: messageTs,
text: "workspace",
})
}
const handleRestoreBoth = () => {
setRestoreBothDisabled(true)
vscode.postMessage({
type: "checkpointRestore",
number: messageTs,
text: "taskAndWorkspace",
})
}
const handleMouseEnter = () => {
setHasMouseEntered(true)
}
const handleMouseLeave = () => {
if (hasMouseEntered) {
setShowRestoreConfirm(false)
setHasMouseEntered(false)
}
}
const handleControlsMouseLeave = (e: React.MouseEvent) => {
const tooltipElement = tooltipRef.current
if (tooltipElement && showRestoreConfirm) {
const tooltipRect = tooltipElement.getBoundingClientRect()
if (
e.clientY >= tooltipRect.top &&
e.clientY <= tooltipRect.bottom &&
e.clientX >= tooltipRect.left &&
e.clientX <= tooltipRect.right
) {
return
}
}
setShowRestoreConfirm(false)
setHasMouseEntered(false)
}
useEvent("message", handleMessage)
return (
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
<i
className="codicon codicon-bookmark"
style={{
color: isCheckpointCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)",
fontSize: "12px",
flexShrink: 0,
}}
/>
<Label $isCheckedOut={isCheckpointCheckedOut}>
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
</Label>
<DottedLine $isCheckedOut={isCheckpointCheckedOut} />
<ButtonGroup>
<CustomButton
$isCheckedOut={isCheckpointCheckedOut}
disabled={compareDisabled}
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
onClick={() => {
setCompareDisabled(true)
vscode.postMessage({
type: "checkpointDiff",
number: messageTs,
})
}}>
Compare
</CustomButton>
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
<div ref={refs.setReference} style={{ position: "relative", marginTop: -2 }}>
<CustomButton
$isCheckedOut={isCheckpointCheckedOut}
isActive={showRestoreConfirm}
onClick={() => setShowRestoreConfirm(true)}>
Restore
</CustomButton>
{showRestoreConfirm &&
createPortal(
<RestoreConfirmTooltip
ref={refs.setFloating}
style={floatingStyles}
data-placement={placement}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreWorkspace}
disabled={restoreWorkspaceDisabled}
style={{
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
width: "100%",
marginBottom: "10px",
}}>
Restore Files
</VSCodeButton>
<p>
Restores your project's files back to a snapshot taken at this point (use "Compare" to see
what will be reverted)
</p>
</RestoreOption>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreTask}
disabled={restoreTaskDisabled}
style={{
cursor: restoreTaskDisabled ? "wait" : "pointer",
width: "100%",
marginBottom: "10px",
}}>
Restore Task Only
</VSCodeButton>
<p>Deletes messages after this point (does not affect workspace files)</p>
</RestoreOption>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreBoth}
disabled={restoreBothDisabled}
style={{
cursor: restoreBothDisabled ? "wait" : "pointer",
width: "100%",
marginBottom: "10px",
}}>
Restore Files & Task
</VSCodeButton>
<p>Restores your project's files and deletes all messages after this point</p>
</RestoreOption>
</RestoreConfirmTooltip>,
document.body,
)}
</div>
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
</ButtonGroup>
</Container>
)
}
const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
display: flex;
align-items: center;
padding: 4px 0;
gap: 4px;
position: relative;
min-width: 0;
margin-top: -10px;
margin-bottom: -10px;
opacity: ${(props) => (props.$isCheckedOut ? 1 : props.isMenuOpen ? 1 : 0.5)};
&:hover {
opacity: 1;
}
`
const Label = styled.span<{ $isCheckedOut?: boolean }>`
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
font-size: 9px;
flex-shrink: 0;
`
const DottedLine = styled.div<{ small?: boolean; $isCheckedOut?: boolean }>`
flex: ${(props) => (props.small ? "0 0 5px" : "1")};
min-width: ${(props) => (props.small ? "5px" : "5px")};
height: 1px;
background-image: linear-gradient(
to right,
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")} 50%,
transparent 50%
);
background-size: 4px 1px;
background-repeat: repeat-x;
`
const ButtonGroup = styled.div`
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
`
const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $isCheckedOut?: boolean }>`
background: ${(props) =>
props.isActive || props.disabled
? props.$isCheckedOut
? "var(--vscode-textLink-foreground)"
: "var(--vscode-descriptionForeground)"
: "transparent"};
border: none;
color: ${(props) =>
props.isActive || props.disabled
? "var(--vscode-editor-background)"
: props.$isCheckedOut
? "var(--vscode-textLink-foreground)"
: "var(--vscode-descriptionForeground)"};
padding: 2px 6px;
font-size: 9px;
cursor: pointer;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 1px;
background-image: ${(props) =>
props.isActive || props.disabled
? "none"
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
background-position:
0 0,
100% 0,
0 100%,
0 0;
}
&:hover:not(:disabled) {
background: ${(props) =>
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
color: var(--vscode-editor-background);
&::before {
display: none;
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`
const RestoreOption = styled.div`
&:not(:last-child) {
margin-bottom: 10px;
padding-bottom: 4px;
border-bottom: 1px solid var(--vscode-editorGroup-border);
}
p {
margin: 0 0 2px 0;
color: var(--vscode-descriptionForeground);
font-size: 11px;
line-height: 14px;
}
&:last-child p {
margin: 0 0 -2px 0;
}
`
const RestoreConfirmTooltip = styled.div`
position: fixed;
background: ${CODE_BLOCK_BG_COLOR};
border: 1px solid var(--vscode-editorGroup-border);
padding: 12px;
border-radius: 3px;
width: min(calc(100vw - 54px), 600px);
z-index: 1000;
// Add invisible padding to create a safe hover zone
&::before {
content: "";
position: absolute;
top: -8px;
left: 0;
right: 0;
height: 8px;
}
// Adjust arrow to be above the padding
&::after {
content: "";
position: absolute;
top: -6px;
right: 24px;
width: 10px;
height: 10px;
background: ${CODE_BLOCK_BG_COLOR};
border-left: 1px solid var(--vscode-editorGroup-border);
border-top: 1px solid var(--vscode-editorGroup-border);
transform: rotate(45deg);
z-index: 1;
}
// When menu appears above the button
&[data-placement^="top"] {
&::before {
top: auto;
bottom: -8px;
}
&::after {
top: auto;
bottom: -6px;
right: 24px;
transform: rotate(225deg);
}
}
p {
margin: 0 0 6px 0;
color: var(--vscode-descriptionForeground);
font-size: 12px;
white-space: normal;
word-wrap: break-word;
}
`
@@ -120,7 +120,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
if (!node.lang) {
node.lang = "javascript"
} else if (node.lang.includes(".")) {
// if the langauge is a file, get the extension
// if the language is a file, get the extension
node.lang = node.lang.split(".").slice(-1)[0]
}
})
@@ -0,0 +1,30 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
const StyledButton = styled(VSCodeButton)`
--danger-button-bg: #c42b2b;
--danger-button-hover: #a82424;
--danger-button-active: #8f1f1f;
background-color: var(--danger-button-bg) !important;
border-color: var(--danger-button-bg) !important;
color: #ffffff !important;
&:hover {
background-color: var(--danger-button-hover) !important;
border-color: var(--danger-button-hover) !important;
}
&:active {
background-color: var(--danger-button-active) !important;
border-color: var(--danger-button-active) !important;
}
`
interface DangerButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
const DangerButton: React.FC<DangerButtonProps> = (props) => {
return <StyledButton {...props} />
}
export default DangerButton
@@ -1,10 +1,11 @@
import { memo, useEffect } from "react"
import React, { memo, useEffect } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
import MermaidBlock from "./MermaidBlock"
interface MarkdownBlockProps {
markdown?: string
@@ -50,6 +51,44 @@ const remarkUrlToLink = () => {
}
}
/**
* Custom remark plugin that prevents filenames with extensions from being parsed as bold text
* For example: __init__.py should not be rendered as bold "init" followed by ".py"
* Solves https://github.com/cline/cline/issues/1028
*/
const remarkPreventBoldFilenames = () => {
return (tree: any) => {
visit(tree, "strong", (node: any, index: number | undefined, parent: any) => {
// Only process if there's a next node (potential file extension)
if (!parent || typeof index === "undefined" || index === parent.children.length - 1) return
const nextNode = parent.children[index + 1]
// Check if next node is text and starts with . followed by extension
if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) return
// If the strong node has multiple children, something weird is happening
if (node.children?.length !== 1) return
// Get the text content from inside the strong node
const strongContent = node.children?.[0]?.value
if (!strongContent || typeof strongContent !== "string") return
// Validate that the strong content is a valid filename
if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) return
// Combine into a single text node
const newNode = {
type: "text",
value: `__${strongContent}__${nextNode.value}`,
}
// Replace both nodes with the combined text node
parent.children.splice(index, 2, newNode)
})
}
}
const StyledMarkdown = styled.div`
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
@@ -160,6 +199,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
remarkPreventBoldFilenames,
remarkUrlToLink,
() => {
return (tree) => {
@@ -181,7 +221,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
pre: ({ node, children, ...preProps }: any) => {
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
const child = children[0] as React.ReactElement<{ className?: string }>
if (child.props?.className?.includes("language-mermaid")) {
return child
}
}
return (
<StyledPre {...preProps} theme={theme}>
{children}
</StyledPre>
)
},
code: (props: any) => {
const className = props.className || ""
if (className.includes("language-mermaid")) {
const codeText = String(props.children || "")
return <MermaidBlock code={codeText} />
}
return <code {...props} />
},
},
},
})
@@ -0,0 +1,227 @@
import { useEffect, useRef, useState } from "react"
import mermaid from "mermaid"
import { useDebounceEffect } from "../../utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "../../utils/vscode"
const MERMAID_THEME = {
background: "#1e1e1e", // VS Code dark theme background
textColor: "#ffffff", // Main text color
mainBkg: "#2d2d2d", // Background for nodes
nodeBorder: "#888888", // Border color for nodes
lineColor: "#cccccc", // Lines connecting nodes
primaryColor: "#3c3c3c", // Primary color for highlights
primaryTextColor: "#ffffff", // Text in primary colored elements
primaryBorderColor: "#888888",
secondaryColor: "#2d2d2d", // Secondary color for alternate elements
tertiaryColor: "#454545", // Third color for special elements
// Class diagram specific
classText: "#ffffff",
// State diagram specific
labelColor: "#ffffff",
// Sequence diagram specific
actorLineColor: "#cccccc",
actorBkg: "#2d2d2d",
actorBorder: "#888888",
actorTextColor: "#ffffff",
// Flow diagram specific
fillType0: "#2d2d2d",
fillType1: "#3c3c3c",
fillType2: "#454545",
}
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
theme: "dark",
themeVariables: {
...MERMAID_THEME,
fontSize: "16px",
fontFamily: "var(--vscode-font-family, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif)",
// Additional styling
noteTextColor: "#ffffff",
noteBkgColor: "#454545",
noteBorderColor: "#888888",
// Improve contrast for special elements
critBorderColor: "#ff9580",
critBkgColor: "#803d36",
// Task diagram specific
taskTextColor: "#ffffff",
taskTextOutsideColor: "#ffffff",
taskTextLightColor: "#ffffff",
// Numbers/sections
sectionBkgColor: "#2d2d2d",
sectionBkgColor2: "#3c3c3c",
// Alt sections in sequence diagrams
altBackground: "#2d2d2d",
// Links
linkColor: "#6cb6ff",
// Borders and lines
compositeBackground: "#2d2d2d",
compositeBorder: "#888888",
titleColor: "#ffffff",
},
})
interface MermaidBlockProps {
code: string
}
export default function MermaidBlock({ code }: MermaidBlockProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isLoading, setIsLoading] = useState(false)
// 1) Whenever `code` changes, mark that we need to re-render a new chart
useEffect(() => {
setIsLoading(true)
}, [code])
// 2) Debounce the actual parse/render
useDebounceEffect(
() => {
if (containerRef.current) {
containerRef.current.innerHTML = ""
}
mermaid
.parse(code, { suppressErrors: true })
.then((isValid) => {
if (!isValid) {
throw new Error("Invalid or incomplete Mermaid code")
}
const id = `mermaid-${Math.random().toString(36).substring(2)}`
return mermaid.render(id, code)
})
.then(({ svg }) => {
if (containerRef.current) {
containerRef.current.innerHTML = svg
}
})
.catch((err) => {
console.warn("Mermaid parse/render failed:", err)
containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
})
.finally(() => {
setIsLoading(false)
})
},
500, // Delay 500ms
[code], // Dependencies for scheduling
)
/**
* Called when user clicks the rendered diagram.
* Converts the <svg> to a PNG and sends it to the extension.
*/
const handleClick = async () => {
if (!containerRef.current) return
const svgEl = containerRef.current.querySelector("svg")
if (!svgEl) return
try {
const pngDataUrl = await svgToPng(svgEl)
vscode.postMessage({
type: "openImage",
text: pngDataUrl,
})
} catch (err) {
console.error("Error converting SVG to PNG:", err)
}
}
return (
<MermaidBlockContainer>
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
{/* The container for the final <svg> or raw code. */}
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
</MermaidBlockContainer>
)
}
async function svgToPng(svgEl: SVGElement): Promise<string> {
console.log("svgToPng function called")
// Clone the SVG to avoid modifying the original
const svgClone = svgEl.cloneNode(true) as SVGElement
// Get the original viewBox
const viewBox = svgClone.getAttribute("viewBox")?.split(" ").map(Number) || []
const originalWidth = viewBox[2] || svgClone.clientWidth
const originalHeight = viewBox[3] || svgClone.clientHeight
// Calculate the scale factor to fit editor width while maintaining aspect ratio
// Unless we can find a way to get the actual editor window dimensions through the VS Code API (which might be possible but would require changes to the extension side),
// the fixed width seems like a reliable approach.
const editorWidth = 3_600
const scale = editorWidth / originalWidth
const scaledHeight = originalHeight * scale
// Update SVG dimensions
svgClone.setAttribute("width", `${editorWidth}`)
svgClone.setAttribute("height", `${scaledHeight}`)
const serializer = new XMLSerializer()
const svgString = serializer.serializeToString(svgClone)
const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement("canvas")
canvas.width = editorWidth
canvas.height = scaledHeight
const ctx = canvas.getContext("2d")
if (!ctx) return reject("Canvas context not available")
// Fill background with Mermaid's dark theme background color
ctx.fillStyle = MERMAID_THEME.background
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
ctx.drawImage(img, 0, 0, editorWidth, scaledHeight)
resolve(canvas.toDataURL("image/png", 1.0))
}
img.onerror = reject
img.src = svgDataUrl
})
}
const MermaidBlockContainer = styled.div`
position: relative;
margin: 8px 0;
`
const LoadingMessage = styled.div`
padding: 8px 0;
color: var(--vscode-descriptionForeground);
font-style: italic;
font-size: 0.9em;
`
interface SvgContainerProps {
$isLoading: boolean
}
const SvgContainer = styled.div<SvgContainerProps>`
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
min-height: 20px;
transition: opacity 0.2s ease;
cursor: pointer;
display: flex;
justify-content: center;
`
@@ -0,0 +1,56 @@
import React, { useState } from "react"
import styled from "styled-components"
import {
getAsVar,
VSC_DESCRIPTION_FOREGROUND,
VSC_SIDEBAR_BACKGROUND,
VSC_INPUT_PLACEHOLDER_FOREGROUND,
VSC_INPUT_BORDER,
} from "../../utils/vscStyles"
interface TooltipProps {
visible: boolean
hintText: string
tipText: string
children: React.ReactNode
}
// add styled component for tooltip
const TooltipBody = styled.div`
position: absolute;
background-color: ${getAsVar(VSC_SIDEBAR_BACKGROUND)};
color: ${getAsVar(VSC_DESCRIPTION_FOREGROUND)};
padding: 5px;
border-radius: 5px;
bottom: 100%;
left: -180%;
z-index: 10;
white-space: wrap;
max-width: 200px;
border: 1px solid ${getAsVar(VSC_INPUT_BORDER)};
pointer-events: none;
font-size: 0.9em;
`
const Hint = styled.div`
font-size: 0.8em;
color: ${getAsVar(VSC_INPUT_PLACEHOLDER_FOREGROUND)};
opacity: 0.8;
margin-top: 2px;
`
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children }) => {
return (
<div style={{ position: "relative", display: "inline-block" }}>
{children}
{visible && (
<TooltipBody>
{tipText}
{hintText && <Hint>{hintText}</Hint>}
</TooltipBody>
)}
</div>
)
}
export default Tooltip
+176 -125
View File
@@ -1,78 +1,40 @@
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { vscode } from "../../utils/vscode"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { useEffect, useState } from "react"
import styled from "styled-components"
import { McpServer } from "../../../../src/shared/mcp"
import McpToolRow from "./McpToolRow"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { getMcpServerDisplayName } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import DangerButton from "../common/DangerButton"
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
import McpResourceRow from "./McpResourceRow"
import McpToolRow from "./McpToolRow"
type McpViewProps = {
onDone: () => void
}
const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers } = useExtensionState()
const { mcpServers: servers, mcpMarketplaceEnabled } = useExtensionState()
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
// const [servers, setServers] = useState<McpServer[]>([
// // Add some mock servers for testing
// {
// name: "local-tools",
// config: JSON.stringify({
// mcpServers: {
// "local-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-tools"],
// },
// },
// }),
// status: "connected",
// tools: [
// {
// name: "execute_command",
// description: "Run a shell command on the local system",
// },
// {
// name: "read_file",
// description: "Read contents of a file from the filesystem",
// },
// ],
// },
// {
// name: "postgres-db",
// config: JSON.stringify({
// mcpServers: {
// "postgres-db": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
// },
// },
// }),
// status: "disconnected",
// error: "Failed to connect to database: Connection refused",
// },
// {
// name: "github-tools",
// config: JSON.stringify({
// mcpServers: {
// "github-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-github"],
// },
// },
// }),
// status: "connecting",
// resources: [
// {
// uri: "github://repo/issues",
// name: "Repository Issues",
// },
// {
// uri: "github://repo/pulls",
// name: "Pull Requests",
// },
// ],
// },
// ])
const handleTabChange = (tab: string) => {
setActiveTab(tab)
}
useEffect(() => {
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
// If marketplace is disabled and we're on marketplace tab, switch to installed
setActiveTab("installed")
}
}, [mcpMarketplaceEnabled, activeTab])
useEffect(() => {
if (mcpMarketplaceEnabled) {
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
}
}, [mcpMarketplaceEnabled])
return (
<div
@@ -90,86 +52,146 @@ const McpView = ({ onDone }: McpViewProps) => {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 17px 10px 20px",
padding: "10px 17px 5px 20px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<div style={{ flex: 1, overflow: "auto" }}>
{/* Tabs container */}
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "16px",
marginTop: "5px",
display: "flex",
gap: "1px",
padding: "0 20px 0 20px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
The{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>{" "}
enables communication with locally running MCP servers that provide additional tools and resources to extend
Cline's capabilities. You can use{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
community-made servers
</VSCodeLink>{" "}
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
{mcpMarketplaceEnabled && (
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
Marketplace
</TabButton>
)}
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
Installed
</TabButton>
</div>
{servers.length > 0 && (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}>
{servers.map((server) => (
<ServerRow key={server.name} server={server} />
))}
</div>
)}
{/* Content container */}
<div style={{ width: "100%" }}>
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
{activeTab === "installed" && (
<div style={{ padding: "16px 20px" }}>
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "16px",
marginTop: "5px",
}}>
The{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>{" "}
enables communication with locally running MCP servers that provide additional tools and resources
to extend Cline's capabilities. You can use{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
community-made servers
</VSCodeLink>{" "}
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
npm docs").{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
</div>
{/* Server Configuration Button */}
{servers.length > 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}>
{servers.map((server) => (
<ServerRow key={server.name} server={server} />
))}
</div>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "12px",
marginTop: 20,
marginBottom: 20,
color: "var(--vscode-descriptionForeground)",
}}>
No MCP servers installed
</div>
)}
<div style={{ marginTop: "10px", width: "100%" }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
{/* Settings Section */}
<div style={{ marginBottom: "20px", marginTop: 10 }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%", marginBottom: "5px" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
<div style={{ textAlign: "center" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
</div>
</div>
)}
</div>
{/* Advanced Settings Link */}
<div style={{ textAlign: "center", marginTop: "5px" }}>
<VSCodeLink
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "cline.mcp",
})
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div>
</div>
)
}
const StyledTabButton = styled.button<{ isActive: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
margin-bottom: -1px;
font-family: inherit;
&:hover {
color: var(--vscode-foreground);
}
`
const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
{children}
</StyledTabButton>
)
// Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => {
const { mcpMarketplaceCatalog } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const getStatusColor = () => {
switch (server.status) {
@@ -195,6 +217,14 @@ const ServerRow = ({ server }: { server: McpServer }) => {
})
}
const handleDelete = () => {
setIsDeleting(true)
vscode.postMessage({
type: "deleteMcpServer",
serverName: server.name,
})
}
return (
<div style={{ marginBottom: "10px" }}>
<div
@@ -211,7 +241,18 @@ const ServerRow = ({ server }: { server: McpServer }) => {
{!server.error && (
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
)}
<span style={{ flex: 1 }}>{server.name}</span>
<span
style={{
flex: 1,
overflow: "hidden",
wordBreak: "break-all",
whiteSpace: "normal",
display: "flex",
alignItems: "center",
marginRight: "4px",
}}>
{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
</span>
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
<div
role="switch"
@@ -379,6 +420,16 @@ const ServerRow = ({ server }: { server: McpServer }) => {
}}>
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
</VSCodeButton>
<DangerButton
onClick={handleDelete}
disabled={isDeleting}
style={{
width: "calc(100% - 14px)",
margin: "5px 7px 3px 7px",
}}>
{isDeleting ? "Deleting..." : "Delete Server"}
</DangerButton>
</div>
)
)}
@@ -0,0 +1,302 @@
import { useCallback, useState, useRef, useMemo } from "react"
import styled from "styled-components"
import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp"
import { vscode } from "../../../utils/vscode"
import { useEvent } from "react-use"
interface McpMarketplaceCardProps {
item: McpMarketplaceItem
installedServers: McpServer[]
}
const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => {
const isInstalled = installedServers.some((server) => server.name === item.mcpId)
const [isDownloading, setIsDownloading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const githubLinkRef = useRef<HTMLDivElement>(null)
const handleMessage = useCallback((event: MessageEvent) => {
const message = event.data
switch (message.type) {
case "mcpDownloadDetails":
setIsDownloading(false)
break
case "relinquishControl":
setIsLoading(false)
break
}
}, [])
useEvent("message", handleMessage)
const githubAuthorUrl = useMemo(() => {
const url = new URL(item.githubUrl)
const pathParts = url.pathname.split("/")
if (pathParts.length >= 2) {
return `${url.origin}/${pathParts[1]}`
}
return item.githubUrl
}, [item.githubUrl])
return (
<>
<style>
{`
.mcp-card {
cursor: pointer;
outline: none !important;
}
.mcp-card:hover {
background-color: var(--vscode-list-hoverBackground);
}
.mcp-card:focus {
outline: none !important;
}
`}
</style>
<a
href={item.githubUrl}
className="mcp-card"
style={{
padding: "14px 16px",
display: "flex",
flexDirection: "column",
gap: 12,
cursor: isLoading ? "wait" : "pointer",
textDecoration: "none",
color: "inherit",
}}>
{/* Main container with logo and content */}
<div style={{ display: "flex", gap: "12px" }}>
{/* Logo */}
{item.logoUrl && (
<img
src={item.logoUrl}
alt={`${item.name} logo`}
style={{
width: 42,
height: 42,
borderRadius: 4,
}}
/>
)}
{/* Content section */}
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}>
{/* First row: name and install button */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "16px",
}}>
<h3
style={{
margin: 0,
fontSize: "13px",
fontWeight: 600,
}}>
{item.name}
</h3>
<div
onClick={(e) => {
e.preventDefault() // Prevent card click when clicking install
e.stopPropagation() // Stop event from bubbling up to parent link
if (!isInstalled && !isDownloading) {
setIsDownloading(true)
vscode.postMessage({
type: "downloadMcp",
mcpId: item.mcpId,
})
}
}}
style={{}}>
<StyledInstallButton disabled={isInstalled || isDownloading} $isInstalled={isInstalled}>
{isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"}
</StyledInstallButton>
</div>
</div>
{/* Second row: metadata */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
flexWrap: "wrap",
minWidth: 0,
rowGap: 0,
}}>
<a
href={githubAuthorUrl}
style={{
display: "flex",
alignItems: "center",
color: "var(--vscode-foreground)",
minWidth: 0,
opacity: 0.7,
textDecoration: "none",
border: "none !important",
}}
className="github-link"
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "1"
e.currentTarget.style.color = "var(--link-active-foreground)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "0.7"
e.currentTarget.style.color = "var(--vscode-foreground)"
}}>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }} ref={githubLinkRef}>
<span className="codicon codicon-github" style={{ fontSize: "14px" }} />
<span
style={{
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-all",
minWidth: 0,
}}>
{item.author}
</span>
</div>
</a>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-star-full" />
<span style={{ wordBreak: "break-all" }}>{item.githubStars?.toLocaleString() ?? 0}</span>
</div>
{/* <div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
minWidth: 0,
flexShrink: 0,
}}>
<span className="codicon codicon-cloud-download" />
<span style={{ wordBreak: "break-all" }}>{item.downloadCount?.toLocaleString() ?? 0}</span>
</div> */}
{item.requiresApiKey && (
<span className="codicon codicon-key" title="Requires API key" style={{ flexShrink: 0 }} />
)}
</div>
</div>
</div>
{/* Description and tags */}
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{/* {!item.isRecommended && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "12px",
color: "var(--vscode-notificationsWarningIcon-foreground)",
marginTop: -3,
marginBottom: -3,
}}>
<span className="codicon codicon-warning" style={{ fontSize: "14px" }} />
<span>Community Made (use at your own risk)</span>
</div>
)} */}
<p style={{ fontSize: "13px", margin: 0 }}>{item.description}</p>
<div
style={{
display: "flex",
gap: "6px",
flexWrap: "nowrap",
overflow: "hidden",
position: "relative",
}}>
<span
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
{item.category}
</span>
{item.tags.map((tag, index) => (
<span
key={tag}
style={{
fontSize: "10px",
padding: "1px 4px",
borderRadius: "3px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
display: "inline-flex",
}}>
{tag}
{index === item.tags.length - 1 ? "" : ""}
</span>
))}
<div
style={{
position: "absolute",
right: 0,
top: 0,
bottom: 0,
width: "32px",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
pointerEvents: "none",
}}
/>
</div>
</div>
</a>
</>
)
}
const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>`
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 2px;
border: none;
cursor: pointer;
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
color: var(--vscode-button-foreground);
&:hover:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryHoverBackground)" : "var(--vscode-button-hoverBackground)"};
}
&:active:not(:disabled) {
background: ${(props) =>
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
opacity: 0.7;
}
&:disabled {
opacity: 0.5;
cursor: default;
}
`
export default McpMarketplaceCard
@@ -0,0 +1,286 @@
import { useEffect, useMemo, useState } from "react"
import {
VSCodeButton,
VSCodeProgressRing,
VSCodeRadioGroup,
VSCodeRadio,
VSCodeDropdown,
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name">("newest")
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
}, [items])
const filteredItems = useMemo(() => {
return items
.filter((item) => {
const matchesSearch =
searchQuery === "" ||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
const matchesCategory = !selectedCategory || item.category === selectedCategory
return matchesSearch && matchesCategory
})
.sort((a, b) => {
switch (sortBy) {
// case "downloadCount":
// return b.downloadCount - a.downloadCount
case "stars":
return b.githubStars - a.githubStars
case "name":
return a.name.localeCompare(b.name)
case "newest":
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
default:
return 0
}
})
}, [items, searchQuery, selectedCategory, sortBy])
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "mcpMarketplaceCatalog") {
if (message.error) {
setError(message.error)
} else {
setItems(message.mcpMarketplaceCatalog?.items || [])
setError(null)
}
setIsLoading(false)
setIsRefreshing(false)
} else if (message.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
}
}
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog
fetchMarketplace()
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
const fetchMarketplace = (forceRefresh: boolean = false) => {
if (forceRefresh) {
setIsRefreshing(true)
} else {
setIsLoading(true)
}
setError(null)
vscode.postMessage({ type: "fetchMcpMarketplace", bool: forceRefresh })
}
if (isLoading || isRefreshing) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
}}>
<VSCodeProgressRing />
</div>
)
}
if (error) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
gap: "12px",
}}>
<div style={{ color: "var(--vscode-errorForeground)" }}>{error}</div>
<VSCodeButton appearance="secondary" onClick={() => fetchMarketplace(true)}>
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
Retry
</VSCodeButton>
</div>
)
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
}}>
<div style={{ padding: "20px 20px 5px", display: "flex", flexDirection: "column", gap: "16px" }}>
{/* Search row */}
<VSCodeTextField
style={{ width: "100%" }}
placeholder="Search MCPs..."
value={searchQuery}
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}>
<div
slot="start"
className="codicon codicon-search"
style={{
fontSize: 13,
opacity: 0.8,
}}
/>
{searchQuery && (
<div
className="codicon codicon-close"
aria-label="Clear search"
onClick={() => setSearchQuery("")}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
cursor: "pointer",
}}
/>
)}
</VSCodeTextField>
{/* Filter row */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
flexShrink: 0,
}}>
Filter:
</span>
<div
style={{
position: "relative",
zIndex: 2,
flex: 1,
}}>
<VSCodeDropdown
style={{
width: "100%",
}}
value={selectedCategory || ""}
onChange={(e) => setSelectedCategory((e.target as HTMLSelectElement).value || null)}>
<VSCodeOption value="">All Categories</VSCodeOption>
{categories.map((category) => (
<VSCodeOption key={category} value={category}>
{category}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
</div>
{/* Sort row */}
<div
style={{
display: "flex",
gap: "8px",
}}>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
textTransform: "uppercase",
fontWeight: 500,
marginTop: "3px",
}}>
Sort:
</span>
<VSCodeRadioGroup
style={{
display: "flex",
flexWrap: "wrap",
marginTop: "-2.5px",
}}
value={sortBy}
onChange={(e) => setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}>
{/* <VSCodeRadio value="downloadCount">Most Installs</VSCodeRadio> */}
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="stars">GitHub Stars</VSCodeRadio>
<VSCodeRadio value="name">Name</VSCodeRadio>
</VSCodeRadioGroup>
</div>
</div>
<style>
{`
.mcp-search-input,
.mcp-select {
box-sizing: border-box;
}
.mcp-search-input {
min-width: 140px;
}
.mcp-search-input:focus,
.mcp-select:focus {
border-color: var(--vscode-focusBorder) !important;
}
.mcp-search-input:hover,
.mcp-select:hover {
opacity: 0.9;
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column" }}>
{filteredItems.length === 0 ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
padding: "20px",
color: "var(--vscode-descriptionForeground)",
}}>
{searchQuery || selectedCategory
? "No matching MCP servers found"
: "No MCP servers found in the marketplace"}
</div>
) : (
filteredItems.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
)}
<McpSubmitCard />
</div>
</div>
)
}
export default McpMarketplaceView
@@ -0,0 +1,45 @@
const McpSubmitCard = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "12px",
padding: "15px",
margin: "20px",
backgroundColor: "var(--vscode-textBlockQuote-background)",
borderRadius: "6px",
}}>
{/* Icon */}
<i className="codicon codicon-add" style={{ fontSize: "18px" }} />
{/* Content */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "4px",
textAlign: "center",
maxWidth: "480px",
}}>
<h3
style={{
margin: 0,
fontSize: "14px",
fontWeight: 600,
color: "var(--vscode-foreground)",
}}>
Submit MCP Server
</h3>
<p style={{ fontSize: "13px", margin: 0, color: "var(--vscode-descriptionForeground)" }}>
Help others discover great MCP servers by submitting an issue to{" "}
<a href="https://github.com/cline/mcp-marketplace">github.com/cline/mcp-marketplace</a>
</p>
</div>
</div>
)
}
export default McpSubmitCard
+362 -31
View File
@@ -20,6 +20,8 @@ import {
bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
qwenDefaultModelId,
qwenModels,
geminiDefaultModelId,
geminiModels,
mistralDefaultModelId,
@@ -39,6 +41,7 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
import styled from "styled-components"
import * as vscodemodels from "vscode"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
interface ApiOptionsProps {
showModelOptions: boolean
@@ -78,6 +81,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
@@ -133,7 +137,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
@@ -177,16 +181,20 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}}>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="mistral">Mistral</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="requesty">Requesty</VSCodeOption>
<VSCodeOption value="together">Together</VSCodeOption>
<VSCodeOption value="qwen">Alibaba Qwen</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
@@ -309,6 +317,62 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "qwen" && (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="china">China API</VSCodeOption>
<VSCodeOption value="international">International API</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Please select the appropriate API interface based on your location. If you are in China, choose the China
API interface. Otherwise, choose the International API interface.
</p>
<VSCodeTextField
value={apiConfiguration?.qwenApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("qwenApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Qwen API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.qwenApiKey && (
<VSCodeLink
href="https://bailian.console.aliyun.com/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Qwen API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{selectedProvider === "mistral" && (
<div>
<VSCodeTextField
@@ -382,30 +446,56 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
flexDirection: "column",
gap: 5,
}}>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</>
)}
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
@@ -460,9 +550,18 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
from this extension.
{apiConfiguration?.awsUseProfile ? (
<>
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
profile. These credentials are only used locally to make API requests from this extension.
</>
) : (
<>
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API
requests from this extension.
</>
)}
</p>
</div>
)}
@@ -597,6 +696,189 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
/>
)}
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
display: "flex",
margin: "10px 0",
cursor: "pointer",
alignItems: "center",
}}
onClick={() => setModelConfigurationSelected((val) => !val)}>
<span
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
style={{
marginRight: "4px",
}}></span>
<span
style={{
fontWeight: 700,
textTransform: "uppercase",
}}>
Model Configuration
</span>
</div>
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={apiConfiguration?.openAiModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.openAiModelInfo?.contextWindow
? apiConfiguration.openAiModelInfo.contextWindow.toString()
: openAiModelInfoSaneDefaults.contextWindow?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
apiConfiguration?.openAiModelInfo?.maxTokens
? apiConfiguration.openAiModelInfo.maxTokens.toString()
: openAiModelInfoSaneDefaults.maxTokens?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.openAiModelInfo?.inputPrice
? apiConfiguration.openAiModelInfo.inputPrice.toString()
: openAiModelInfoSaneDefaults.inputPrice?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = input.target.value
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
</VSCodeTextField>
<VSCodeTextField
value={
apiConfiguration?.openAiModelInfo?.outputPrice
? apiConfiguration.openAiModelInfo.outputPrice.toString()
: openAiModelInfoSaneDefaults.outputPrice?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = input.target.value
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
</VSCodeTextField>
</div>
</>
)}
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
{selectedProvider === "requesty" && (
<div>
<VSCodeTextField
value={apiConfiguration?.requestyApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("requestyApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.requestyModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("requestyModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
{selectedProvider === "together" && (
<div>
<VSCodeTextField
value={apiConfiguration?.togetherApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("togetherApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
@@ -739,6 +1021,46 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "litellm" && (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. gpt-4"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
</div>
)}
{selectedProvider === "ollama" && (
<div>
<VSCodeTextField
@@ -828,6 +1150,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "qwen" && createDropdown(qwenModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
</DropdownContainer>
@@ -1035,6 +1358,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "qwen":
return getProviderData(qwenModels, qwenDefaultModelId)
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@@ -1072,6 +1397,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
@@ -228,8 +228,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet:beta")}>
anthropic/claude-3.5-sonnet:beta.
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
anthropic/claude-3.5-sonnet.
</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</>
@@ -0,0 +1,152 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import ApiOptions from "../ApiOptions"
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "requesty",
requestyApiKey: "",
requestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
})),
}
})
describe("ApiOptions Component", () => {
vi.clearAllMocks()
const mockPostMessage = vi.fn()
beforeEach(() => {
global.vscode = { postMessage: mockPostMessage } as any
})
it("renders Requesty API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Requesty Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
})
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "together",
requestyApiKey: "",
requestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
})),
}
})
describe("ApiOptions Component", () => {
vi.clearAllMocks()
const mockPostMessage = vi.fn()
beforeEach(() => {
global.vscode = { postMessage: mockPostMessage } as any
})
it("renders Together API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Together Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
})
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "openai",
requestyApiKey: "",
requestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
})),
}
})
describe("OpenApiInfoOptions", () => {
const mockPostMessage = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
global.vscode = { postMessage: mockPostMessage }
})
it("renders OpenAI Supports Images input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByText("Supports Images")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders OpenAI Context Window Size input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const orgIdInput = screen.getByText("Context Window Size")
expect(orgIdInput).toBeInTheDocument()
})
it("renders OpenAI Max Output Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const modelInput = screen.getByText("Max Output Tokens")
expect(modelInput).toBeInTheDocument()
})
})
@@ -1,10 +1,10 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings"
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage"
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
import { findLastIndex } from "../../../src/shared/array"
import { McpServer } from "../../../src/shared/mcp"
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "../../../src/shared/BrowserSettings"
@@ -17,6 +17,7 @@ interface ExtensionStateContextType extends ExtensionState {
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
@@ -37,6 +38,7 @@ export const ExtensionStateContextProvider: React.FC<{
browserSettings: DEFAULT_BROWSER_SETTINGS,
chatSettings: DEFAULT_CHAT_SETTINGS,
isLoggedIn: false,
platform: DEFAULT_PLATFORM,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -48,7 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
@@ -64,9 +66,13 @@ export const ExtensionStateContextProvider: React.FC<{
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
].some((key) => key !== undefined)
@@ -116,6 +122,12 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -133,6 +145,7 @@ export const ExtensionStateContextProvider: React.FC<{
openRouterModels,
openAiModels,
mcpServers,
mcpMarketplaceCatalog,
filePaths,
setApiConfiguration: (value) =>
setState((prevState) => ({
@@ -0,0 +1,64 @@
import { renderHook } from "@testing-library/react"
import { useShortcut, useMetaKeyDetection } from "../hooks"
import { vi } from "vitest"
describe("useShortcut", () => {
it("should call the callback when the shortcut is pressed", () => {
const callback = vi.fn()
renderHook(() => useShortcut("Meta+Shift+a", callback))
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
window.dispatchEvent(event)
expect(callback).toHaveBeenCalled()
})
it("should not call the callback when the shortcut is not pressed", () => {
const callback = vi.fn()
renderHook(() => useShortcut("Command+Shift+b", callback))
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
window.dispatchEvent(event)
expect(callback).not.toHaveBeenCalled()
})
it("should not call the callback when typing in a text input when disableTextInputs is true", () => {
const callback = vi.fn()
renderHook(() => useShortcut("Meta+Shift+a", callback, { disableTextInputs: true }))
const input = document.createElement("input")
document.body.appendChild(input)
input.focus()
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
input.dispatchEvent(event)
expect(callback).not.toHaveBeenCalled()
document.body.removeChild(input)
})
})
describe("useMetaKeyDetection", () => {
it("should detect Windows OS and metaKey from platform", () => {
// mock the detect functions
const { result } = renderHook(() => useMetaKeyDetection("win32"))
expect(result.current[0]).toBe("windows")
expect(result.current[1]).toBe("Win")
})
it("should detect Mac OS and metaKey from platform", () => {
// mock the detect functions
const { result } = renderHook(() => useMetaKeyDetection("darwin"))
expect(result.current[0]).toBe("mac")
expect(result.current[1]).toBe("CMD")
})
it("should detect Linux OS and metaKey from platform", () => {
// mock the detect functions
const { result } = renderHook(() => useMetaKeyDetection("linux"))
expect(result.current[0]).toBe("linux")
expect(result.current[1]).toBe("Alt")
})
})
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest"
import { detectMetaKeyChar } from "../platformUtils"
describe("detectMetaKeyChar", () => {
it("should return ⌘ Command for darwin platform", () => {
const result = detectMetaKeyChar("darwin")
expect(result).toBe("CMD")
})
it("should return ⊞ Win for win32 platform", () => {
const result = detectMetaKeyChar("win32")
expect(result).toBe("Win")
})
it("should return Alt for linux platform", () => {
const result = detectMetaKeyChar("linux")
expect(result).toBe("Alt")
})
it("should return generic CMD for unknown platform", () => {
const result = detectMetaKeyChar("somethingelse")
expect(result).toBe("CMD")
})
})
+95 -11
View File
@@ -1,4 +1,5 @@
import { mentionRegex } from "../../../src/shared/context-mentions"
import { Fzf } from "fzf"
export function insertMention(text: string, position: number, value: string): { newValue: string; mentionIndex: number } {
const beforeCursor = text.slice(0, position)
@@ -46,13 +47,17 @@ export enum ContextMenuOptionType {
File = "file",
Folder = "folder",
Problems = "problems",
Terminal = "terminal",
URL = "url",
Git = "git",
NoResults = "noResults",
}
export interface ContextMenuQueryItem {
type: ContextMenuOptionType
value?: string
label?: string
description?: string
}
export function getContextMenuOptions(
@@ -60,6 +65,13 @@ export function getContextMenuOptions(
selectedType: ContextMenuOptionType | null = null,
queryItems: ContextMenuQueryItem[],
): ContextMenuQueryItem[] {
const workingChanges: ContextMenuQueryItem = {
type: ContextMenuOptionType.Git,
value: "git-changes",
label: "Working changes",
description: "Current uncommitted changes",
}
if (query === "") {
if (selectedType === ContextMenuOptionType.File) {
const files = queryItems
@@ -81,30 +93,102 @@ export function getContextMenuOptions(
return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }]
}
if (selectedType === ContextMenuOptionType.Git) {
const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git)
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
}
return [
{ type: ContextMenuOptionType.URL },
{ type: ContextMenuOptionType.Problems },
{ type: ContextMenuOptionType.Terminal },
{ type: ContextMenuOptionType.Git },
{ type: ContextMenuOptionType.Folder },
{ type: ContextMenuOptionType.File },
]
}
const lowerQuery = query.toLowerCase()
const suggestions: ContextMenuQueryItem[] = []
// Check for top-level option matches
if ("git".startsWith(lowerQuery)) {
suggestions.push({
type: ContextMenuOptionType.Git,
label: "Git Commits",
description: "Search repository history",
})
} else if ("git-changes".startsWith(lowerQuery)) {
suggestions.push(workingChanges)
}
if ("problems".startsWith(lowerQuery)) {
suggestions.push({ type: ContextMenuOptionType.Problems })
}
if (query.startsWith("http")) {
return [{ type: ContextMenuOptionType.URL, value: query }]
} else {
const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery))
suggestions.push({ type: ContextMenuOptionType.URL, value: query })
}
if (matchingItems.length > 0) {
return matchingItems.map((item) => ({
type: item.type,
value: item.value,
}))
// Add exact SHA matches to suggestions
if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) {
const exactMatches = queryItems.filter(
(item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery,
)
if (exactMatches.length > 0) {
suggestions.push(...exactMatches)
} else {
return [{ type: ContextMenuOptionType.NoResults }]
// If no exact match but valid SHA format, add as option
suggestions.push({
type: ContextMenuOptionType.Git,
value: lowerQuery,
label: `Commit ${lowerQuery}`,
description: "Git commit hash",
})
}
}
// Create searchable strings array for fzf
const searchableItems = queryItems.map((item) => ({
original: item,
searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "),
}))
// Initialize fzf instance for fuzzy search
const fzf = new Fzf(searchableItems, {
selector: (item) => item.searchStr,
})
// Get fuzzy matching items
const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : []
// Separate matches by type
const fileMatches = matchingItems.filter(
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
)
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
const otherMatches = matchingItems.filter(
(item) =>
item.type !== ContextMenuOptionType.File &&
item.type !== ContextMenuOptionType.Folder &&
item.type !== ContextMenuOptionType.Git,
)
// Combine suggestions with matching items in the desired order
if (suggestions.length > 0 || matchingItems.length > 0) {
const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
// Remove duplicates based on type and value
const seen = new Set()
const deduped = allItems.filter((item) => {
const key = `${item.type}-${item.value}`
if (seen.has(key)) return false
seen.add(key)
return true
})
return deduped
}
return [{ type: ContextMenuOptionType.NoResults }]
}
export function shouldShowContextMenu(text: string, position: number): boolean {
@@ -121,8 +205,8 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
// Don't show the menu if it's a URL
if (textAfterAt.toLowerCase().startsWith("http")) return false
// Don't show the menu if it's a problems
if (textAfterAt.toLowerCase().startsWith("problems")) return false
// Don't show the menu if it's a problems or terminal
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks

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